@@ -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,73 @@ 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 ( "reasoning preset models emit max_completion_tokens, never max_tokens" , ( ) => {
133+ for ( const model of openaiApi ?. maxCompletionTokensModels ?? [ ] ) {
134+ const body = wireBody ( model , presetBaseURL ) ;
135+ expect ( body [ "max_completion_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
136+ expect ( "max_tokens" in body ) . toBe ( false ) ;
137+ }
138+ } ) ;
139+
140+ test ( "non-reasoning preset models keep max_tokens" , ( ) => {
141+ const declared = new Set ( openaiApi ?. maxCompletionTokensModels ?? [ ] ) ;
142+ const rest = presetModels . filter ( ( m ) => ! declared . has ( m ) ) ;
143+ expect ( rest . length ) . toBeGreaterThan ( 0 ) ;
144+ for ( const model of rest ) {
145+ const body = wireBody ( model , presetBaseURL ) ;
146+ expect ( body [ "max_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
147+ expect ( "max_completion_tokens" in body ) . toBe ( false ) ;
148+ }
149+ } ) ;
150+
151+ test ( "relay endpoint keeps max_tokens for every preset model" , ( ) => {
152+ for ( const model of presetModels ) {
153+ const body = wireBody ( model , RELAY_BASE_URL ) ;
154+ expect ( body [ "max_tokens" ] ) . toBe ( SOURCE_MAX_TOKENS ) ;
155+ expect ( "max_completion_tokens" in body ) . toBe ( false ) ;
156+ }
157+ } ) ;
158+ } ) ;
0 commit comments