Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,51 @@ describe("buildOpenAISource", () => {
});
});

test("overrides maxTokens and adds temperature to providerOptions", () => {
const source = buildOpenAISource({
id: "fp",
baseURL: "https://fp/v1",
apiKey: "k",
model: "m",
maxTokens: 8192,
temperature: 0.7,
});
expect(source.defaults).toEqual({
maxTokens: 8192,
providerOptions: { temperature: 0.7 },
});
});

test("adds top_p to providerOptions when topP is present", () => {
const source = buildOpenAISource({
id: "fp",
baseURL: "https://fp/v1",
apiKey: "k",
model: "m",
topP: 0.9,
});
expect(source.defaults).toEqual({
maxTokens: SOURCE_MAX_TOKENS,
providerOptions: { top_p: 0.9 },
});
});

test("coexists temperature with reasoning_effort in one providerOptions", () => {
const source = buildOpenAISource({
id: "fp",
baseURL: "https://fp/v1",
apiKey: "k",
model: "gpt-5.1",
reasoningEffort: "medium",
temperature: 1.0,
maxTokens: 4096,
});
expect(source.defaults).toEqual({
maxTokens: 4096,
providerOptions: { reasoning_effort: "medium", temperature: 1.0 },
});
});

test("projects an Ollama root URL to the OpenAI-compatible /v1 endpoint", () => {
const source = buildOpenAISource({
id: "ollama/default",
Expand Down
73 changes: 67 additions & 6 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,17 @@ export function buildOpenAISource(fields: {
apiKey?: string;
model: string;
reasoningEffort?: ReasoningEffort;
maxTokens?: number;
temperature?: number;
topP?: number;
quirks?: Record<string, unknown>;
}): InferenceSource {
const overrides =
fields.reasoningEffort !== undefined
? { providerOptions: { reasoning_effort: fields.reasoningEffort } }
: {};
const providerOptions: Record<string, unknown> = {};
if (fields.reasoningEffort !== undefined)
providerOptions["reasoning_effort"] = fields.reasoningEffort;
if (fields.temperature !== undefined)
providerOptions["temperature"] = fields.temperature;
if (fields.topP !== undefined) providerOptions["top_p"] = fields.topP;
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
Expand All @@ -287,7 +292,10 @@ export function buildOpenAISource(fields: {
: normalizeOpenAICompatibleBaseURL(fields.baseURL),
credentialId: fields.id,
model: fields.model,
defaults: { maxTokens: SOURCE_MAX_TOKENS, ...overrides },
defaults: {
maxTokens: fields.maxTokens ?? SOURCE_MAX_TOKENS,
...(Object.keys(providerOptions).length > 0 ? { providerOptions } : {}),
},
...(fields.quirks !== undefined ? { quirks: fields.quirks } : {}),
};
}
Expand Down Expand Up @@ -1374,12 +1382,55 @@ export function mergeOAuthCatalog(
export async function refreshLiveProviderCatalog(
settings: Settings | null,
resolved: ResolvedProvider,
liveSelection?: () => Pick<ResolvedProvider, "providerName" | "model">,
): Promise<ProviderCatalogEntry[]> {
const [codexProfiles, xaiProfiles] = await Promise.all([
listCodexProfiles(),
listXaiProfiles(),
]);
return mergeOAuthCatalog(settings, resolved, codexProfiles, xaiProfiles);
const catalog = mergeOAuthCatalog(
settings,
resolved,
codexProfiles,
xaiProfiles,
);
// Discovery can finish after a model switch; never restore its old bare-model slot.
const active = liveSelection?.() ?? resolved;
refreshProviderContextWindows(
settings ?? undefined,
catalog,
active.providerName,
active.model,
);
return catalog;
}

export function refreshProviderContextWindows(
settings: Settings | undefined,
catalog: readonly ProviderCatalogEntry[],
activeProvider: string,
activeModel: string,
): void {
// Catalog models can expand during discovery. OAuth projections deliberately
// omit settings-only window overrides, just as startup resolution does.
const providers = Object.fromEntries(
catalog.map((entry) => {
const window =
entry.codexProfile === undefined && entry.xaiProfile === undefined
? settings?.providers[entry.name]?.contextWindow
: undefined;
return [
entry.name,
{
models: entry.models,
...(window !== undefined ? { contextWindow: window } : {}),
},
];
}),
);
setProviderContextWindowOverrides(
buildProviderContextWindowOverrides(providers, activeProvider, activeModel),
);
}

export function catalogEntryAsProviderSettings(
Expand All @@ -1405,6 +1456,11 @@ export function catalogEntryAsProviderSettings(
? { defaultModel: entry.defaultModel }
: {}),
...(entry.free !== undefined ? { free: entry.free } : {}),
...(entry.maxTokens !== undefined ? { maxTokens: entry.maxTokens } : {}),
...(entry.temperature !== undefined
? { temperature: entry.temperature }
: {}),
...(entry.topP !== undefined ? { topP: entry.topP } : {}),
...(entry.bifrostVirtualKey === true ? { bifrostVirtualKey: true } : {}),
...(entry.anthropic === true ? { anthropic: true } : {}),
...(go ? { opencodeGo: true } : {}),
Expand Down Expand Up @@ -1482,6 +1538,11 @@ export function buildProviderCatalog(
? { defaultModel: p.defaultModel }
: {}),
...(p.free !== undefined ? { free: p.free } : {}),
...(p.maxTokens !== undefined ? { maxTokens: p.maxTokens } : {}),
...(p.temperature !== undefined
? { temperature: p.temperature }
: {}),
...(p.topP !== undefined ? { topP: p.topP } : {}),
...(p.bifrostVirtualKey === true ? { bifrostVirtualKey: true } : {}),
...(p.anthropic === true ? { anthropic: true } : {}),
...(go ? { opencodeGo: true } : {}),
Expand Down
61 changes: 61 additions & 0 deletions src/config/inference-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,67 @@ describe("contextWindow / maxTokens split (CL-7784)", () => {
});
});

describe("custom provider token/sampling knobs flow into the source", () => {
test("maxTokens/temperature/topP from provider settings reach defaults", () => {
const entryCatalog: ProviderCatalogEntry[] = [
{
name: "custom",
baseURL: "https://api.example.com/v1",
apiKey: "sk-key",
models: ["m"],
},
];
const settings: Settings = {
providers: {
custom: {
baseURL: "https://api.example.com/v1",
apiKey: "sk-key",
models: ["m"],
maxTokens: 4096,
temperature: 0.7,
},
},
};
const source = buildInferenceSourceForRef(
{ provider: "custom", model: "m" },
{ sessionId: "sess-1", catalog: entryCatalog },
settings,
);
expect(source).not.toBeNull();
expect(source?.defaults?.maxTokens).toBe(4096);
expect(source?.defaults?.providerOptions).toEqual({ temperature: 0.7 });
});

test("topP flows as top_p and overrides nothing else", () => {
const entryCatalog: ProviderCatalogEntry[] = [
{
name: "custom",
baseURL: "https://api.example.com/v1",
apiKey: "sk-key",
models: ["m"],
topP: 0.9,
},
];
const source = buildInferenceSourceForRef(
{ provider: "custom", model: "m" },
{ sessionId: "sess-1", catalog: entryCatalog },
undefined,
);
expect(source?.defaults?.maxTokens).toBe(SOURCE_MAX_TOKENS);
expect(source?.defaults?.providerOptions).toEqual({ top_p: 0.9 });
});

test("unset knobs leave the default maxTokens and no providerOptions", () => {
const source = buildInferenceSourceForRef(
{ provider: "fp", model: "fp-large" },
ctx(),
undefined,
);
expect(source?.defaults?.maxTokens).toBe(SOURCE_MAX_TOKENS);
expect(source?.defaults?.providerOptions).toBeUndefined();
});
});

describe("OpenAI reasoning max_completion_tokens quirk (CL-7785)", () => {
const openaiApi = firstClassProviderById("openai")?.paths?.find(
(p) => p.id === "api",
Expand Down
6 changes: 6 additions & 0 deletions src/config/inference-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export function buildInferenceSourceForRef(
}

const quirks = openAISourceQuirks(baseURL, ref.model);
const maxTokens = entry?.maxTokens ?? providerSettings?.maxTokens;
const temperature = entry?.temperature ?? providerSettings?.temperature;
const topP = entry?.topP ?? providerSettings?.topP;
return buildOpenAISource({
id: ref.provider,
baseURL,
Expand All @@ -189,6 +192,9 @@ export function buildInferenceSourceForRef(
: {}),
model: ref.model,
...(effort !== undefined ? { reasoningEffort: effort } : {}),
...(maxTokens !== undefined ? { maxTokens } : {}),
...(temperature !== undefined ? { temperature } : {}),
...(topP !== undefined ? { topP } : {}),
...(quirks !== undefined ? { quirks } : {}),
});
}
Expand Down
Loading
Loading