Porting a Script That Hits Six Vendors Separately Into One Aggregator Base URL
A practical port plan for replacing six vendor-specific SDK integrations with one aggregator base URL and a single API key, including what changes per SDK, what stays vendor-specific, and how to verify behavioral parity model by model.
Why consolidate at all
A script that talks to six vendors usually carries six sets of credentials, six retry policies, six response-shape adapters and six places to update when a model name changes. Most of these vendors expose an OpenAI-compatible chat completions surface, which is what makes a single base URL practical.
The aggregator does not remove vendor-specific behaviour; it moves the transport, auth and billing into one layer so your application code only deals with one envelope.
Inventory before you touch code
Before editing anything, write down what you actually depend on. For each vendor integration record:
- The SDK or HTTP client used, and its version.
- The auth mechanism (bearer token, custom header, query parameter).
- The endpoint path and whether it is chat completions, responses, or a bespoke route.
- Request fields you set: system prompt placement, temperature, max tokens, stop sequences, tool definitions, streaming flag.
- Response fields you read: content path, finish reason, usage counters, tool call structure, refusal or safety fields.
- Error classes you branch on and how you retry them.
- Any vendor-only features in use, such as a specific reasoning mode, prompt caching directive, or file upload.
This table becomes your parity checklist later. Anything you cannot express through the unified surface has to be either dropped, emulated, or kept on a direct integration.
Decide the target shape
Most aggregators expose an OpenAI-compatible endpoint. That gives you two migration styles:
- Keep each SDK, change the base URL and key. Lowest code churn, but you still carry six SDKs and six response adapters. Some vendor SDKs accept a
base_urloverride; others do not, and forcing it can break path construction. - Collapse onto one client. Replace all six with a single OpenAI-compatible client and select the model per request. More churn up front, but the codebase ends up with one retry path, one error model and one place to add models.
For a script with six vendors, the second option is usually the one that pays off. If any single vendor cannot be expressed compatibly, keep that one on a direct path and route the rest through the aggregator.
What actually changes per vendor
When you move to a single base URL and key, the per-vendor changes are mostly mechanical:
- Auth: every bearer token, API key header and signed request becomes the aggregator key. Delete the per-vendor secret loading.
- Base URL: point the client at the aggregator endpoint; keep the completions path as the SDK expects it.
- Model identifiers: replace each vendor's native model name with the aggregator's identifier for the same model. Do this from a mapping table, not string edits scattered through the code.
- Response parsing: if you keep SDKs, response shapes stay as they were. If you collapse onto one client, normalise once and delete the six adapters.
- Errors: vendor-specific exception types become one HTTP status and error body. Map rate limit, auth failure, invalid request and server error to your own internal categories.
- Usage and cost: read usage from the unified response and compute cost from the published multiplier rather than each vendor's pricing page.
- Streaming: confirm the aggregator emits the same delta events your code already consumes; if you used a vendor SDK's stream helper, that helper now parses the unified stream.
What stays vendor-specific
Do not assume the unified surface makes models interchangeable. These remain per-model decisions:
- Prompt formatting: system message support, multi-turn conventions and any model-specific instruction style.
- Tool calling: schema dialect, parallel call support, and how results are echoed back.
- Sampling parameters: which knobs a given model honours, and what happens to ones it ignores.
- Context limits and output caps: still per model; validate before sending.
- Special modes: reasoning toggles, structured output guarantees, caching hints. If the aggregator passes them through, keep them; if not, treat as a capability gap.
- Token counting: tokenisers differ, so usage numbers from the response are the only reliable source.
Step-by-step port plan
- Freeze the current behaviour. Capture request and response fixtures from each vendor for a small set of representative prompts. Store them, including streaming transcripts.
- Create the model mapping. One table: internal alias to vendor model to aggregator model identifier. All model selection goes through it.
- Introduce an abstraction seam. Put every model call behind one function with a stable signature. This is the only place that knows about transport.
- Add the aggregator client first, without removing vendors. Route one low-risk model through the new path behind a flag.
- Move vendors one at a time. Change the base URL, key and model identifier for a single vendor, run the fixture suite, then move the next. Never port two vendors in the same commit.
- Collapse the adapters. Once all traffic is on the unified client, delete the per-vendor SDK dependencies and response mappers.
- Remove dead credentials. Revoke old keys and delete them from configuration and CI secrets.
- Re-check billing expectations. Confirm the price you now pay reflects the aggregator multiplier, and that usage reporting reconciles against your internal counters.
Verifying parity per model
Parity means the same prompt produces behaviour your application can still consume, not byte-identical output. LLM outputs are non-deterministic, so compare structure and invariants:
- Schema checks: content present, finish reason valid, usage counters populated, tool calls parse under the same schema.
- Semantic checks: run a small evaluation set per model and compare against the pre-port fixtures using a rubric or a judge model. Flag material regressions, not wording differences.
- Stop conditions: verify max tokens, stop sequences and length cutoffs behave as before.
- Streaming: assert delta ordering, final chunk and error mid-stream handling.
- Error paths: force a 401, a 429 and an invalid model name, and confirm your code categorises and retries them correctly.
- Concurrency: run a burst at your normal parallelism and confirm no new throttling appears in the unified layer.
Keep this suite; it is how you validate the next model you add, and how you notice when an upstream model changes.
Operational notes
- One key, many models: budget and rotate a single credential instead of six. Keep environment separation anyway, with different keys for staging and production.
- Cost visibility: users pay the official price times 1.3, so your internal cost model should read usage from responses rather than estimating with tokenisers.
- Fallbacks: the unified surface makes model fallback cheap; define a fallback order per task instead of per vendor.
- Feature gaps: document any model you deliberately keep on a direct integration and why.
When not to consolidate
If a vendor feature you depend on is not reachable through the unified API, or if you need vendor-specific support channels, leave that integration direct. A single base URL is a convenience layer, not a reason to lose capability.