Skip to content

Commit c91556c

Browse files
author
octane0411
committed
Fix sessions_spawn Google/Gemini model routing
1 parent 2c8ee59 commit c91556c

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

src/agents/model-selection.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,49 @@ export function inferUniqueProviderFromConfiguredModels(params: {
204204
return providers.values().next().value;
205205
}
206206

207+
function inferProviderFromModelHint(params: {
208+
cfg: OpenClawConfig;
209+
model: string;
210+
}): string | undefined {
211+
const inferredProvider = inferUniqueProviderFromConfiguredModels({
212+
cfg: params.cfg,
213+
model: params.model,
214+
});
215+
if (inferredProvider) {
216+
return inferredProvider;
217+
}
218+
const lower = params.model.toLowerCase();
219+
if (lower === "gemini" || lower.startsWith("gemini-")) {
220+
return "google";
221+
}
222+
return undefined;
223+
}
224+
225+
function normalizeSubagentSpawnModelSelection(params: {
226+
cfg: OpenClawConfig;
227+
model: string;
228+
defaultProvider: string;
229+
}): string {
230+
const trimmed = params.model.trim();
231+
if (!trimmed) {
232+
return trimmed;
233+
}
234+
if (trimmed.includes("/")) {
235+
const parsed = parseModelRef(trimmed, params.defaultProvider);
236+
if (!parsed) {
237+
return trimmed;
238+
}
239+
return `${parsed.provider}/${parsed.model}`;
240+
}
241+
const inferredProvider = inferProviderFromModelHint({
242+
cfg: params.cfg,
243+
model: trimmed,
244+
});
245+
const selectedProvider = normalizeProviderId(inferredProvider ?? params.defaultProvider);
246+
const selectedModel = normalizeProviderModelId(selectedProvider, trimmed);
247+
return `${selectedProvider}/${selectedModel}`;
248+
}
249+
207250
export function resolveAllowlistModelKey(raw: string, defaultProvider: string): string | null {
208251
const parsed = parseModelRef(raw, defaultProvider);
209252
if (!parsed) {
@@ -371,13 +414,38 @@ export function resolveSubagentSpawnModelSelection(params: {
371414
cfg: params.cfg,
372415
agentId: params.agentId,
373416
});
417+
const resolvedModelOverride = params.modelOverride
418+
? normalizeSubagentSpawnModelSelection({
419+
cfg: params.cfg,
420+
model: normalizeModelSelection(params.modelOverride) ?? "",
421+
defaultProvider: runtimeDefault.provider,
422+
})
423+
: undefined;
424+
const resolvedConfiguredModel = resolveSubagentConfiguredModelSelection({
425+
cfg: params.cfg,
426+
agentId: params.agentId,
427+
});
428+
const normalizedConfiguredModel = resolvedConfiguredModel
429+
? normalizeSubagentSpawnModelSelection({
430+
cfg: params.cfg,
431+
model: resolvedConfiguredModel,
432+
defaultProvider: runtimeDefault.provider,
433+
})
434+
: undefined;
435+
const normalizedPrimaryModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)
436+
? normalizeSubagentSpawnModelSelection({
437+
cfg: params.cfg,
438+
model:
439+
normalizeModelSelection(
440+
resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model),
441+
) ?? "",
442+
defaultProvider: runtimeDefault.provider,
443+
})
444+
: undefined;
374445
return (
375-
normalizeModelSelection(params.modelOverride) ??
376-
resolveSubagentConfiguredModelSelection({
377-
cfg: params.cfg,
378-
agentId: params.agentId,
379-
}) ??
380-
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
446+
resolvedModelOverride ??
447+
normalizedConfiguredModel ??
448+
normalizedPrimaryModel ??
381449
`${runtimeDefault.provider}/${runtimeDefault.model}`
382450
);
383451
}

src/agents/openclaw-tools.subagents.sessions-spawn.model.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => {
134134
);
135135
expect(patchCall?.params).toMatchObject({
136136
key: expect.stringContaining("subagent:"),
137-
model: "claude-haiku-4-5",
137+
model: "anthropic/claude-haiku-4-5",
138138
});
139139
});
140140

@@ -252,7 +252,7 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => {
252252
acceptedAtBase: 4000,
253253
patch: async (request) => {
254254
const model = (request.params as { model?: unknown } | undefined)?.model;
255-
if (model === "bad-model") {
255+
if (model === "anthropic/bad-model" || model === "bad-model") {
256256
throw new Error("invalid model: bad-model");
257257
}
258258
return { ok: true };
@@ -271,11 +271,42 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => {
271271
});
272272
expect(result.details).toMatchObject({
273273
status: "error",
274+
modelApplied: false,
274275
});
275276
expect(String((result.details as { error?: string }).error ?? "")).toContain("invalid model");
276277
expect(calls.some((call) => call.method === "agent")).toBe(false);
277278
});
278279

280+
it("sessions_spawn maps bare Gemini model names to the Google provider", async () => {
281+
const calls: GatewayCall[] = [];
282+
mockLongRunningSpawnFlow({
283+
calls,
284+
acceptedAtBase: 4500,
285+
});
286+
287+
const tool = await getSessionsSpawnTool({
288+
agentSessionKey: "main",
289+
agentChannel: "discord",
290+
});
291+
292+
const result = await tool.execute("call-gemini", {
293+
task: "do thing",
294+
model: "gemini-2.5-flash",
295+
runTimeoutSeconds: 1,
296+
});
297+
expect(result.details).toMatchObject({
298+
status: "accepted",
299+
modelApplied: true,
300+
});
301+
302+
const patchCall = calls.find(
303+
(call) => call.method === "sessions.patch" && (call.params as { model?: string })?.model,
304+
);
305+
expect(patchCall?.params).toMatchObject({
306+
model: "google/gemini-2.5-flash",
307+
});
308+
});
309+
279310
it("sessions_spawn supports legacy timeoutSeconds alias", async () => {
280311
let spawnedTimeout: number | undefined;
281312

src/agents/subagent-spawn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ export async function spawnSubagentDirect(
437437
if (modelPatchError) {
438438
return {
439439
status: "error",
440+
modelApplied: false,
440441
error: modelPatchError,
441442
childSessionKey,
442443
};

0 commit comments

Comments
 (0)