fix(openai): map system prompts to top-level instructions for ChatGPT auth requests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user