diff --git a/changelog.md b/changelog.md index 315be8b..34fe48f 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,13 @@ ## Pending +### Fixes + +- map system prompts to top-level instructions for ChatGPT auth requests (openai) + - wrap OpenAI models using ChatGPT auth with middleware that extracts system messages into provider instructions + - remove system messages from the serialized prompt payload to match the ChatGPT Codex backend expectations + - add test coverage to verify authorization headers, workspace routing, and instruction payload mapping + ## 2026-05-14 - 4.0.0 ### Breaking Changes diff --git a/test/test.openai-auth.node.ts b/test/test.openai-auth.node.ts index 4757ab8..3b45163 100644 --- a/test/test.openai-auth.node.ts +++ b/test/test.openai-auth.node.ts @@ -199,15 +199,21 @@ tap.test('getModel uses ChatGPT Codex backend for OpenAI ChatGPT auth', async () }; try { - await model.doGenerate({ - prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], - inputFormat: 'prompt', - } as any); + await smartai.generateText({ + model, + system: 'system prompt', + prompt: 'hello', + }); expect(capturedRequest?.url).toEqual('https://chatgpt.com/backend-api/codex/responses'); expect(getHeader(capturedRequest?.init, 'authorization')).toEqual(`Bearer ${tokenData.accessToken}`); expect(getHeader(capturedRequest?.init, 'chatgpt-account-id')).toEqual('workspace-1'); expect(getHeader(capturedRequest?.init, 'originator')).toEqual('smartai'); + const capturedBody = JSON.parse(String(capturedRequest?.init?.body)); + expect(capturedBody.instructions).toEqual('system prompt'); + expect(capturedBody.input).toEqual([ + { role: 'user', content: [{ type: 'input_text', text: 'hello' }] }, + ]); } finally { globalThis.fetch = originalFetch; } diff --git a/ts/smartai.classes.smartai.ts b/ts/smartai.classes.smartai.ts index a87aeef..964b304 100644 --- a/ts/smartai.classes.smartai.ts +++ b/ts/smartai.classes.smartai.ts @@ -2,6 +2,7 @@ import * as plugins from './plugins.js'; import type { ISmartAiModelSetup, ISmartAiOptions, LanguageModelV3 } from './smartai.interfaces.js'; import { createOllamaModel } from './smartai.provider.ollama.js'; import { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js'; +import { createOpenAiChatGptInstructionsMiddleware } from './smartai.middleware.openai.js'; import { createOpenAiChatGptProviderSettings } from './smartai.auth.openai.js'; /** @@ -28,7 +29,13 @@ export function getModel(options: ISmartAiOptions): LanguageModelV3 { ? createOpenAiChatGptProviderSettings(options.openAiChatGptAuth) : { apiKey: options.apiKey }, ); - return p(options.model) as LanguageModelV3; + const base = p(options.model) as LanguageModelV3; + return options.openAiChatGptAuth + ? plugins.wrapLanguageModel({ + model: base, + middleware: createOpenAiChatGptInstructionsMiddleware(), + }) as unknown as LanguageModelV3 + : base; } case 'google': { const p = plugins.createGoogleGenerativeAI({ apiKey: options.apiKey }); diff --git a/ts/smartai.middleware.openai.ts b/ts/smartai.middleware.openai.ts new file mode 100644 index 0000000..0af1d59 --- /dev/null +++ b/ts/smartai.middleware.openai.ts @@ -0,0 +1,46 @@ +import type { JSONObject, LanguageModelV3CallOptions, LanguageModelV3Middleware } from '@ai-sdk/provider'; + +const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; + +const getSystemInstructions = (prompt: LanguageModelV3CallOptions['prompt']): string | undefined => { + const instructions = prompt + .filter((message) => message.role === 'system') + .map((message) => message.content) + .filter(isNonEmptyString); + + return instructions.length > 0 ? instructions.join('\n') : undefined; +}; + +/** + * ChatGPT's Codex backend requires top-level Responses API instructions. + * The standard OpenAI provider otherwise serializes system prompts as input items. + */ +export function createOpenAiChatGptInstructionsMiddleware(): LanguageModelV3Middleware { + return { + specificationVersion: 'v3', + transformParams: async ({ params }) => { + const instructions = getSystemInstructions(params.prompt); + if (!instructions) { + return params; + } + + const providerOptions = params.providerOptions ?? {}; + const openAiProviderOptions = providerOptions.openai ?? {}; + if (isNonEmptyString(openAiProviderOptions.instructions)) { + return params; + } + + return { + ...params, + prompt: params.prompt.filter((message) => message.role !== 'system'), + providerOptions: { + ...providerOptions, + openai: { + ...openAiProviderOptions, + instructions, + } as JSONObject, + }, + } satisfies LanguageModelV3CallOptions; + }, + }; +}