Skip to content

Commit 9f16963

Browse files
committed
fix(provider): validate custom settings and refresh context limits
2 parents fc82971 + 26406de commit 9f16963

17 files changed

Lines changed: 800 additions & 61 deletions

‎src/agent/tool-schema-normalize.test.ts‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,36 @@ describe("normalizeToolDefinitionsForProvider", () => {
119119
).toBe(false);
120120
});
121121

122-
test("non-kimi providers get identity schemas (recursive present kept)", () => {
122+
// Meta Muse Spark (OpenCode Go / Zen) rejects recursive $ref cycles with
123+
// "Recursive JSON schemas are not currently supported". Same wire rewrite
124+
// as Kimi — model-id gate via isMuseSparkLeafProvider.
125+
test.each([
126+
{
127+
providerName: "opencode-go",
128+
model: "muse-spark-1.3-contributor",
129+
},
130+
{
131+
providerName: "opencode-go",
132+
model: "muse-spark-1.2-contributor",
133+
},
134+
{ providerName: "opencode-go", model: "muse-spark-1.3" },
135+
{ providerName: "zen", model: "muse-spark-1.3-contributor-free" },
136+
] as const)(
137+
"$providerName + $model rewrites present (muse schema gate)",
138+
(ctx) => {
139+
const out = normalizeToolDefinitionsForProvider(defs, ctx);
140+
const present = defined(out.find((d) => d.name === "present"));
141+
expect(schemaHasRef(present.inputSchema)).toBe(false);
142+
expect(schemaHasDefs(present.inputSchema)).toBe(false);
143+
expect(present.inputSchema).toEqual(
144+
structuredClone(
145+
KIMI_PRESENT_INPUT_SCHEMA,
146+
) as typeof present.inputSchema,
147+
);
148+
},
149+
);
150+
151+
test("providers that accept recursive schemas get identity present", () => {
123152
for (const ctx of [
124153
{ providerName: "anthropic", model: "claude-sonnet-4" },
125154
{ providerName: "openai", model: "gpt-5.6" },

‎src/agent/tool-schema-normalize.ts‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { ToolDefinition } from "@intx/types/runtime";
2-
import { isKimiLeafProvider } from "../subagent/provider-family.js";
2+
import {
3+
isKimiLeafProvider,
4+
isMuseSparkLeafProvider,
5+
} from "../subagent/provider-family.js";
36

47
/** Context used to decide whether a provider needs wire-schema rewrites. */
58
export interface NormalizeToolDefsContext {
@@ -9,7 +12,7 @@ export interface NormalizeToolDefsContext {
912

1013
/**
1114
* Shared primitives / view guidance for `present`. Used by both the canonical
12-
* `presentDefinition.description` and the kimi wire `view.description` so the
15+
* `presentDefinition.description` and the acyclic wire `view.description` so the
1316
* two never drift.
1417
*/
1518
export const PRESENT_VIEW_PRIMITIVES_GUIDANCE =
@@ -114,10 +117,11 @@ const COLUMNS_PROP = {
114117
} as const;
115118

116119
/**
117-
* Non-recursive `present` parameters for Moonshot/kimi-class backends.
118-
* Those providers reject JSON Schema `$ref` cycles on `tools.function.parameters`
119-
* (recursive ViewNode). This shape inlines depth-capped oneOf primitives so the
120-
* model still sees type/children/text fields — not a bare freeform object.
120+
* Non-recursive `present` parameters for backends that reject JSON Schema `$ref`
121+
* cycles on `tools.function.parameters` (recursive ViewNode). Moonshot/Kimi and
122+
* Meta Muse Spark both fail the turn before inference when the cycle is on the
123+
* wire. This shape inlines depth-capped oneOf primitives so the model still
124+
* sees type/children/text fields — not a bare freeform object.
121125
* Runtime still validates full nested trees via `validateView`.
122126
*/
123127
export const KIMI_PRESENT_INPUT_SCHEMA = {
@@ -180,7 +184,7 @@ export const KIMI_PRESENT_INPUT_SCHEMA = {
180184
required: ["view"],
181185
} as const;
182186

183-
function rewritePresentForKimi(def: ToolDefinition): ToolDefinition {
187+
function rewritePresentAcyclic(def: ToolDefinition): ToolDefinition {
184188
return {
185189
...def,
186190
// structuredClone so callers cannot mutate the shared const via the tool def.
@@ -190,13 +194,17 @@ function rewritePresentForKimi(def: ToolDefinition): ToolDefinition {
190194
};
191195
}
192196

197+
function needsAcyclicPresentSchema(ctx: NormalizeToolDefsContext): boolean {
198+
return isKimiLeafProvider(ctx) || isMuseSparkLeafProvider(ctx);
199+
}
200+
193201
/**
194202
* Family-gated wire rewrite of tool definitions before they reach the director /
195-
* provider. Today only Moonshot/kimi get a non-recursive `present` schema;
203+
* provider. Moonshot/kimi and Muse Spark get a non-recursive `present` schema;
196204
* other providers receive the definitions unchanged (identity).
197205
*
198206
* Does not alter runtime validation or the canonical `presentDefinition` used
199-
* as the source of truth for non-kimi advertise paths.
207+
* as the source of truth for providers that accept `$ref` cycles.
200208
*
201209
* Call this at every advertise path that may include `present` (main TUI/exec
202210
* sessions). Sub-agent toolsets currently omit `present` (main-session only);
@@ -206,10 +214,10 @@ export function normalizeToolDefinitionsForProvider(
206214
defs: readonly ToolDefinition[],
207215
ctx: NormalizeToolDefsContext,
208216
): ToolDefinition[] {
209-
if (!isKimiLeafProvider(ctx)) {
217+
if (!needsAcyclicPresentSchema(ctx)) {
210218
return defs as ToolDefinition[];
211219
}
212220
return defs.map((def) =>
213-
def.name === "present" ? rewritePresentForKimi(def) : def,
221+
def.name === "present" ? rewritePresentAcyclic(def) : def,
214222
);
215223
}

‎src/config/index.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,12 +1382,55 @@ export function mergeOAuthCatalog(
13821382
export async function refreshLiveProviderCatalog(
13831383
settings: Settings | null,
13841384
resolved: ResolvedProvider,
1385+
liveSelection?: () => Pick<ResolvedProvider, "providerName" | "model">,
13851386
): Promise<ProviderCatalogEntry[]> {
13861387
const [codexProfiles, xaiProfiles] = await Promise.all([
13871388
listCodexProfiles(),
13881389
listXaiProfiles(),
13891390
]);
1390-
return mergeOAuthCatalog(settings, resolved, codexProfiles, xaiProfiles);
1391+
const catalog = mergeOAuthCatalog(
1392+
settings,
1393+
resolved,
1394+
codexProfiles,
1395+
xaiProfiles,
1396+
);
1397+
// Discovery can finish after a model switch; never restore its old bare-model slot.
1398+
const active = liveSelection?.() ?? resolved;
1399+
refreshProviderContextWindows(
1400+
settings ?? undefined,
1401+
catalog,
1402+
active.providerName,
1403+
active.model,
1404+
);
1405+
return catalog;
1406+
}
1407+
1408+
export function refreshProviderContextWindows(
1409+
settings: Settings | undefined,
1410+
catalog: readonly ProviderCatalogEntry[],
1411+
activeProvider: string,
1412+
activeModel: string,
1413+
): void {
1414+
// Catalog models can expand during discovery. OAuth projections deliberately
1415+
// omit settings-only window overrides, just as startup resolution does.
1416+
const providers = Object.fromEntries(
1417+
catalog.map((entry) => {
1418+
const window =
1419+
entry.codexProfile === undefined && entry.xaiProfile === undefined
1420+
? settings?.providers[entry.name]?.contextWindow
1421+
: undefined;
1422+
return [
1423+
entry.name,
1424+
{
1425+
models: entry.models,
1426+
...(window !== undefined ? { contextWindow: window } : {}),
1427+
},
1428+
];
1429+
}),
1430+
);
1431+
setProviderContextWindowOverrides(
1432+
buildProviderContextWindowOverrides(providers, activeProvider, activeModel),
1433+
);
13911434
}
13921435

13931436
export function catalogEntryAsProviderSettings(

0 commit comments

Comments
 (0)