@@ -12,6 +12,7 @@ import {
1212 setProviderContextWindowOverrides ,
1313} from "../provider/context-window.js" ;
1414import { createOpenAICompatibleAdapter } from "../provider/openai-compatible-adapter.js" ;
15+ import { firstClassProviderById } from "../../packages/first-class-providers/src/index.js" ;
1516
1617const WINDOW = 400_000 ;
1718
@@ -85,3 +86,88 @@ describe("contextWindow / maxTokens split (CL-7784)", () => {
8586 expect ( contextWindowFor ( "fp:fp-large" ) ) . toBe ( WINDOW ) ;
8687 } ) ;
8788} ) ;
89+
90+ describe ( "OpenAI reasoning max_completion_tokens quirk (CL-7785)" , ( ) => {
91+ const openaiApi = firstClassProviderById ( "openai" ) ?. paths ?. find (
92+ ( p ) => p . id === "api" ,
93+ ) ;
94+ const presetModels = [ ...( openaiApi ?. models ?? [ ] ) ] ;
95+ const presetBaseURL = openaiApi ?. baseURL ?? "" ;
96+ // A relay serving the same model names through the same adapter but taking
97+ // max_tokens (per the vendor adapter comment) — the quirk must not follow
98+ // the bare model name there.
99+ const RELAY_BASE_URL = "https://opencode.ai/zen/v1" ;
100+
101+ function wireBody ( model : string , baseURL : string ) : Record < string , unknown > {
102+ const entryCatalog : ProviderCatalogEntry [ ] = [
103+ { name : "openai" , baseURL, apiKey : "test-key" , models : [ model ] } ,
104+ ] ;
105+ const source = buildInferenceSourceForRef (
106+ { provider : "openai" , model } ,
107+ { sessionId : "sess-1" , catalog : entryCatalog } ,
108+ undefined ,
109+ ) ;
110+ // Mirror the harness: it resolves the adapter with source.quirks.
111+ const adapter = createOpenAICompatibleAdapter (
112+ source as unknown as Parameters < typeof createOpenAICompatibleAdapter > [ 0 ] ,
113+ source ?. quirks ,
114+ ) ;
115+ const messages = [
116+ { role : "user" , content : [ { type : "text" , text : "hi" } ] } ,
117+ ] as unknown as ConversationTurn [ ] ;
118+ const built = adapter . buildRequest ( messages , model , {
119+ maxTokens : source ?. defaults ?. maxTokens ,
120+ } as InferenceOptions ) ;
121+ return JSON . parse ( built . body ) as Record < string , unknown > ;
122+ }
123+
124+ test ( "shipped preset declares an explicit per-model requirement" , ( ) => {
125+ expect ( presetModels . length ) . toBeGreaterThan ( 0 ) ;
126+ expect ( openaiApi ?. maxCompletionTokensModels ?. length ) . toBeGreaterThan ( 0 ) ;
127+ for ( const model of openaiApi ?. maxCompletionTokensModels ?? [ ] ) {
128+ expect ( presetModels ) . toContain ( model ) ;
129+ }
130+ } ) ;
131+
132+ test ( "every preset model has an explicit quirk decision" , ( ) => {
133+ const flagged = new Set ( openaiApi ?. maxCompletionTokensModels ?? [ ] ) ;
134+ // Explicit max_tokens decision: non-reasoning preset models stay on
135+ // max_tokens. Adding a preset model requires a decision here AND in the
136+ // preset's maxCompletionTokensModels — the union below fails loudly
137+ // otherwise instead of silently sending max_tokens.
138+ const explicitMaxTokensModels = new Set ( [ "gpt-4.1" ] ) ;
139+ expect ( [ ...flagged , ...explicitMaxTokensModels ] . sort ( ) ) . toEqual (
140+ [ ...new Set ( presetModels ) ] . sort ( ) ,
141+ ) ;
142+ expect ( [ ...flagged ] . filter ( ( m ) => explicitMaxTokensModels . has ( m ) ) ) . toEqual (
143+ [ ] ,
144+ ) ;
145+ } ) ;
146+
147+ test ( "reasoning preset models emit max_completion_tokens, never max_tokens" , ( ) => {
148+ for ( const model of openaiApi ?. maxCompletionTokensModels ?? [ ] ) {
149+ const body = wireBody ( model , presetBaseURL ) ;
150+ expect ( body [ "max_completion_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
151+ expect ( "max_tokens" in body ) . toBe ( false ) ;
152+ }
153+ } ) ;
154+
155+ test ( "non-reasoning preset models keep max_tokens" , ( ) => {
156+ const declared = new Set ( openaiApi ?. maxCompletionTokensModels ?? [ ] ) ;
157+ const rest = presetModels . filter ( ( m ) => ! declared . has ( m ) ) ;
158+ expect ( rest . length ) . toBeGreaterThan ( 0 ) ;
159+ for ( const model of rest ) {
160+ const body = wireBody ( model , presetBaseURL ) ;
161+ expect ( body [ "max_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
162+ expect ( "max_completion_tokens" in body ) . toBe ( false ) ;
163+ }
164+ } ) ;
165+
166+ test ( "relay endpoint keeps max_tokens for every preset model" , ( ) => {
167+ for ( const model of presetModels ) {
168+ const body = wireBody ( model , RELAY_BASE_URL ) ;
169+ expect ( body [ "max_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
170+ expect ( "max_completion_tokens" in body ) . toBe ( false ) ;
171+ }
172+ } ) ;
173+ } ) ;
0 commit comments