diff --git a/server/src/addie/model-providers/openai-responses-provider.ts b/server/src/addie/model-providers/openai-responses-provider.ts index c52e1ab1e3..4e981b29de 100644 --- a/server/src/addie/model-providers/openai-responses-provider.ts +++ b/server/src/addie/model-providers/openai-responses-provider.ts @@ -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) { throw new UnexpectedModelIdentityError('openai', request.model, normalized.model); } yield { type: 'response_start', provider: this.id, model: normalized.model, id: normalized.id }; diff --git a/server/tests/unit/addie/fixed-trace-budget.test.ts b/server/tests/unit/addie/fixed-trace-budget.test.ts index bc3be356e3..ba27af8bb6 100644 --- a/server/tests/unit/addie/fixed-trace-budget.test.ts +++ b/server/tests/unit/addie/fixed-trace-budget.test.ts @@ -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); diff --git a/server/tests/unit/addie/model-provider-openai-google.test.ts b/server/tests/unit/addie/model-provider-openai-google.test.ts index 2b58ce9df8..f515cb18c8 100644 --- a/server/tests/unit/addie/model-provider-openai-google.test.ts +++ b/server/tests/unit/addie/model-provider-openai-google.test.ts @@ -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, {