Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,10 @@ export class OpenAIResponsesProvider implements ModelProvider {
{ maxRetries: 0, signal: options.signal },
);
const normalized = normalizeOpenAIResponse(response);
if (normalized.model !== request.model && !normalized.model.startsWith(`${request.model}-`)) {
// Returned model identity is a billing and trust boundary. This adapter has
// no reviewed, literal canonical-alias allowlist, so aliases and suffixes
// must not inherit the requested model's approval or pricing.
if (normalized.model !== request.model) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: The removed startsWith(${request.model}-) clause was load-bearing tolerance for dated snapshot returns. OpenAI's Responses API commonly echoes a dated snapshot for an alias request (the pricing label here is already openai-gpt-5.6-luna-2026-08-26). If the API ever returns gpt-5.6-luna-<date> for the gpt-5.6-luna alias, strict equality now throws UnexpectedModelIdentityError on every happy-path call. Direction is right — fail-closed beats mis-billing an unapproved alias — but confirm the live API returns the exact gpt-5.6-luna identity, or restore a reviewed literal snapshot allowlist (as Google does via isGoogleRouterModelRevision) rather than an open suffix match.

throw new UnexpectedModelIdentityError('openai', request.model, normalized.model);
}
yield { type: 'response_start', provider: this.id, model: normalized.model, id: normalized.id };
Expand Down
4 changes: 2 additions & 2 deletions server/tests/unit/addie/fixed-trace-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ describe('fixed trace provider budget', () => {
.toThrow('cache read accounting is unavailable');
});

it('closes shared admission rather than settling an unapproved returned model at requested rates', async () => {
const mismatched = { ...RESPONSE, model: 'other-openai-model' };
it('closes shared admission rather than settling an attacker-controlled returned model suffix at requested rates', async () => {
const mismatched = { ...RESPONSE, model: 'gpt-5.6-luna-attacker-controlled' };
const delegate = new BudgetScriptedProvider([mismatched, RESPONSE]);
const budget = new FixedTraceBudget(1);
const provider = new BudgetedFixedTraceProvider(delegate, budget, PRICING, RESPONSE_PRICING_POLICY);
Expand Down
33 changes: 33 additions & 0 deletions server/tests/unit/addie/model-provider-openai-google.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,43 @@ describe('OpenAIResponsesProvider', () => {
expect(create).toHaveBeenCalledTimes(1);
expect(create.mock.calls[0][1]).toEqual({ maxRetries: 0, signal: undefined });
expect(beforeDispatch).toHaveBeenCalledTimes(1);
expect(normalized.model).toBe(OPENAI_ROUTER_MODEL);
expect(normalized.finishReason).toBe('stop');
expect(normalized.usage).toEqual({ inputTokens: 10, outputTokens: 5, cacheReadTokens: 2, cacheWriteTokens: 0 });
});

it.each([
'gpt-5.6-luna-attacker-controlled',
'gpt-5.6-luna-2026-01-01',
'gpt-5.6-luna-latest',
'gpt-5.6-terra',
'anthropic-gpt-5.6-luna',
])('rejects the unapproved returned OpenAI model identity %s', async (model) => {
const provider = new OpenAIResponsesProvider('unused', {
responses: { create: vi.fn().mockResolvedValue(openAIResponse({ model })) },
});

await expect(collectModelResponse(provider.respond(request(OPENAI_ROUTER_MODEL))))
.rejects.toMatchObject({
name: 'UnexpectedModelIdentityError',
provider: 'openai',
expectedModel: OPENAI_ROUTER_MODEL,
actualModel: model,
});
});

it.each([
{ model: '', label: 'empty' },
{ model: undefined, label: 'missing' },
])('rejects a $label returned OpenAI model identity', async ({ model }) => {
const provider = new OpenAIResponsesProvider('unused', {
responses: { create: vi.fn().mockResolvedValue(openAIResponse({ model })) },
});

await expect(collectModelResponse(provider.respond(request(OPENAI_ROUTER_MODEL))))
.rejects.toThrow(`Malformed OpenAI response model`);
});

it('projects custom tools and stateless function-call continuation exactly', () => {
const provider = new OpenAIResponsesProvider('unused', {} as OpenAIResponsesTransport);
const prepared = provider.prepare(request(OPENAI_ROUTER_MODEL, {
Expand Down
Loading