/usr/lib/node_modules/openclaw/dist/agent-C-LFFPB6.js:9:import { Bt as resolveAgentTimeoutMs, Cn as resolveOutboundTarget, Gt as registerAgentRunContext, H as getCliSessionId, Ht as emitAgentEvent, J as getRemoteSkillEligibility, Jt as AGENT_LANE_NESTED, On as runWithModelFallback, Ot as runEmbeddedPiAgent, U as setCliSessionId, Vt as clearAgentRunContext, W as runCliAgent, Xt as applyModelOverrideToSessionEntry, Yt as loadModelCatalog, lt as clearSessionAuthProfileOverride, mn as lookupContextTokens, q as resolveSendPolicy, st as getSkillsSnapshotVersion, ut as applyVerboseOverride, wn as resolveSessionDeliveryTarget } from "./loader-Ds3or8QX.js";
/usr/lib/node_modules/openclaw/dist/agent-C-LFFPB6.js:582:			const fallbackResult = await runWithModelFallback({
/usr/lib/node_modules/openclaw/dist/agent-urroxZBf.js:3:import { $t as getSkillsSnapshotVersion, Bt as getRemoteSkillEligibility, Cr as applyModelOverrideToSessionEntry, In as registerAgentRunContext, L as runWithModelFallback, M as resolveOutboundTarget, Mn as clearAgentRunContext, N as resolveSessionDeliveryTarget, Nn as emitAgentEvent, Sr as clearSessionAuthProfileOverride, Wr as resolveAgentTimeoutMs, i as runCliAgent, jn as AGENT_LANE_NESTED, kr as loadModelCatalog, l as runEmbeddedPiAgent, n as getCliSessionId, r as setCliSessionId, s as resolveSendPolicy, wr as applyVerboseOverride, xr as lookupContextTokens } from "./reply-DptDUVRg.js";
/usr/lib/node_modules/openclaw/dist/agent-urroxZBf.js:582:			const fallbackResult = await runWithModelFallback({
/usr/lib/node_modules/openclaw/dist/extensionAPI.js:29536:async function runWithModelFallback(params) {
/usr/lib/node_modules/openclaw/dist/extensionAPI.js:38099:		const fallbackResult = await runWithModelFallback({
/usr/lib/node_modules/openclaw/dist/extensionAPI.js:38537:		await runWithModelFallback({
/usr/lib/node_modules/openclaw/dist/extensionAPI.js:38898:				const fallbackResult = await runWithModelFallback({
/usr/lib/node_modules/openclaw/dist/gateway-cli-DZge3TdL.js:30:import { $ as setSkillsRemoteRegistry, $n as isSystemEventContextChanged, A as resolveHeartbeatVisibility, An as authorizeGatewaySigusr1Restart, Ar as formatUserTime, At as waitForEmbeddedPiRunEnd, B as createReplyDispatcher, Br as registerInternalHook, Bt as resolveAgentTimeoutMs, C as buildControlUiAvatarUrl, Cn as resolveOutboundTarget, Cr as DEFAULT_HEARTBEAT_ACK_MAX_CHARS, Ct as extractImageContentFromSource, Dr as stripHeartbeatToken, Dt as stopSubagentsForRequester, En as resetDirectoryCache, Et as isAbortTrigger, Fn as consumeRestartSentinel, Ft as buildSafeExternalPrompt, Gn as normalizeOptionalAgentId, Gt as registerAgentRunContext, H as getCliSessionId, Hn as normalizeCronJobCreate, Ht as emitAgentEvent, In as formatDoctorNonInteractiveHint, Ir as resolveAgentIdentity, It as detectSuspiciousPatterns, J as getRemoteSkillEligibility, Jn as normalizeRequiredName, K as normalizeSendPolicy, Kn as normalizeOptionalText, Kt as runSubagentAnnounceFlow, L as getChannelActivity, Ln as formatRestartSentinelMessage, Lt as getHookType, M as getLastHeartbeatEvent, Mn as isGatewaySigusr1RestartExternallyAllowed, Mr as resolveUserTimezone, N as onHeartbeatEvent, Nn as scheduleGatewaySigusr1Restart, Nt as registerUnhandledRejectionHandler, O as createReplyPrefixOptions, On as runWithModelFallback, Ot as runEmbeddedPiAgent, Pn as setGatewaySigusr1RestartPolicy, Pt as createOpenClawTools, Q as refreshRemoteNodeBins, Qn as enqueueSystemEvent, Rr as clearInternalHooks, Rt as isExternalHookSession, S as CONTROL_UI_AVATAR_PREFIX, Sr as isDiagnosticsEnabled, St as extractFileContentFromSource, T as resolveAssistantAvatarUrl, Tt as formatZonedTimestamp, U as setCliSessionId, Un as normalizeCronJobPatch, Ut as getAgentRunContext, Vn as writeRestartSentinel, Vr as triggerInternalHook, Vt as clearAgentRunContext, W as runCliAgent, Wn as inferLegacyName, Wt as onAgentEvent, X as recordRemoteNodeInfo, Xt as applyModelOverrideToSessionEntry, Y as primeRemoteSkillsCache, Yn as migrateLegacyCronPayload, Yt as loadModelCatalog, Z as refreshRemoteBinsForConnectedNodes, Zt as loadProviderUsageSummary, _t as DEFAULT_INPUT_MAX_REDIRECTS, an as resolveGatewaySessionStoreTarget, ar as OPENAI_TTS_VOICES, at as updatePairedNodeMetadata, br as startDiagnosticHeartbeat, bt as DEFAULT_INPUT_PDF_MIN_TEXT_CHARS, cn as capArrayByJsonBytes, cr as isTtsProviderConfigured, ct as registerSkillsChangeListener, d as handleReset, dn as resolveSessionTranscriptCandidates, dr as resolveTtsConfig, dt as parseVerboseOverride, et as approveNodePairing, fn as stripEnvelopeFromMessages, fr as resolveTtsPrefsPath, ft as DEFAULT_INPUT_FILE_MAX_BYTES, gn as normalizeGroupActivation, gr as textToSpeech, gt as DEFAULT_INPUT_IMAGE_MIMES, hn as clearSessionQueues, hr as setTtsProvider, ht as DEFAULT_INPUT_IMAGE_MAX_BYTES, in as loadSessionEntry, ir as OPENAI_TTS_MODELS, it as requestNodePairing, jn as consumeGatewaySigusr1RestartAuthorization, jr as resolveUserTimeFormat, k as buildHistoryContextFromEntries, kr as normalizePollInput, kt as abortEmbeddedPiRun, ln as readSessionMessages, lr as resolveTtsApiKey, mn as lookupContextTokens, mr as setTtsEnabled, mt as DEFAULT_INPUT_FILE_MIMES, n as handleSlackHttpRequest, nn as listSessionsFromStore, nt as rejectNodePairing, on as resolveSessionModelRef, or as getTtsProvider, ot as verifyNodeToken, pr as resolveTtsProviderOrder, pt as DEFAULT_INPUT_FILE_MAX_CHARS, q as resolveSendPolicy, qn as normalizePayloadToSystemText, qt as resolveAnnounceTargetFromKey, rn as loadCombinedSessionStoreForGateway, rr as getPluginToolMeta, rt as renamePairedNode, sn as archiveFileOnDisk, sr as isTtsEnabled, st as getSkillsSnapshotVersion, t as loadOpenClawPlugins, tn as listAgentsForGateway, tr as requestHeartbeatNow, tt as listNodePairing, un as readSessionPreviewItemsFromTranscript, ur as resolveTtsAutoMode, ut as applyVerboseOverride, vn as ensureOutboundSessionEntry, vr as setCommandLaneConcurrency, vt as DEFAULT_INPUT_PDF_MAX_PAGES, w as normalizeControlUiBasePath, wn as resolveSessionDeliveryTarget, wt as normalizeMimeList, xr as stopDiagnosticHeartbeat, xt as DEFAULT_INPUT_TIMEOUT_MS, yn as resolveOutboundSessionRoute, yr as CommandLane, yt as DEFAULT_INPUT_PDF_MAX_PIXELS, z as dispatchInboundMessage, zn as summarizeRestartSentinel, zr as createInternalHookEvent, zt as initSubagentRegistry } from "./loader-Ds3or8QX.js";
/usr/lib/node_modules/openclaw/dist/gateway-cli-DZge3TdL.js:2765:		const fallbackResult = await runWithModelFallback({


Found it! The `runWithModelFallback` function is in `extensionAPI.js` at line 29536. Let me look at that function.


	if (params.fallbacksOverride === void 0 && primary?.provider && primary.model) addCandidate({
		provider: primary.provider,
		model: primary.model
	}, false);
	return candidates;
}
async function runWithModelFallback(params) {
	const candidates = resolveFallbackCandidates({
		cfg: params.cfg,
		provider: params.provider,
		model: params.model,
		fallbacksOverride: params.fallbacksOverride
	});
	const authStore = params.cfg ? ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }) : null;
	const attempts = [];
	let lastError;
	for (let i = 0; i < candidates.length; i += 1) {
		const candidate = candidates[i];
		if (authStore) {
			const profileIds = resolveAuthProfileOrder({
				cfg: params.cfg,
				store: authStore,
				provider: candidate.provider
			});
			const isAnyProfileAvailable = profileIds.some((id) => !isProfileInCooldown(authStore, id));
			if (profileIds.length > 0 && !isAnyProfileAvailable) {
				attempts.push({
					provider: candidate.provider,
					model: candidate.model,
					error: `Provider ${candidate.provider} is in cooldown (all profiles unavailable)`,
					reason: "rate_limit"
				});
				continue;
			}
		}
		try {
			return {
				result: await params.run(candidate.provider, candidate.model),
				provider: candidate.provider,
				model: candidate.model,
				attempts
			};
		} catch (err) {
			if (shouldRethrowAbort(err)) throw err;
			const normalized = coerceToFailoverError(err, {
				provider: candidate.provider,
				model: candidate.model
			}) ?? err;
			if (!isFailoverError(normalized)) throw err;
			lastError = normalized;
			const described = describeFailoverError(normalized);
			attempts.push({
				provider: candidate.provider,
				model: candidate.model,
				error: described.message,
				reason: described.reason,
				status: described.status,
				code: described.code
			});
			await params.onError?.({
				provider: candidate.provider,
				model: candidate.model,
				error: normalized,
				attempt: i + 1,
				total: candidates.length
			});
		}
	}
	if (attempts.length <= 1 && lastError) throw lastError;
	const summary = attempts.length > 0 ? attempts.map((attempt) => `${attempt.provider}/${attempt.model}: ${attempt.error}${attempt.reason ? ` (${attempt.reason})` : ""}`).join(" | ") : "unknown";
	throw new Error(`All models failed (${attempts.length || candidates.length}): ${summary}`, { cause: lastError instanceof Error ? lastError : void 0 });
}
async function runWithImageModelFallback(params) {
	const candidates = resolveImageFallbackCandidates({
		cfg: params.cfg,
		defaultProvider: DEFAULT_PROVIDER,
		modelOverride: params.modelOverride
	});
	if (candidates.length === 0) throw new Error("No image model configured. Set agents.defaults.imageModel.primary or agents.defaults.imageModel.fallbacks.");
	const attempts = [];
	let lastError;
	for (let i = 0; i < candidates.length; i += 1) {
		const candidate = candidates[i];
		try {
			return {
				result: await params.run(candidate.provider, candidate.model),
				provider: candidate.provider,
				model: candidate.model,
				attempts
			};
		} catch (err) {
			if (shouldRethrowAbort(err)) throw err;
			lastError = err;
			attempts.push({
				provider: candidate.provider,
				model: candidate.model,
				error: err instanceof Error ? err.message : String(err)
			});
			await params.onError?.({
				provider: candidate.provider,

[38858 more lines in file. Use offset=29630 to continue.]


I can see the `runWithModelFallback` function. It uses `resolveFallbackCandidates` to get the list of models to try. Let me look at that function to see how it builds the candidate list.


	for (const raw of imageFallbacks) addRaw(raw, true);
	return candidates;
}
function resolveFallbackCandidates(params) {
	const primary = params.cfg ? resolveConfiguredModelRef({
		cfg: params.cfg,
		defaultProvider: DEFAULT_PROVIDER,
		defaultModel: DEFAULT_MODEL
	}) : null;
	const defaultProvider = primary?.provider ?? DEFAULT_PROVIDER;
	const defaultModel = primary?.model ?? DEFAULT_MODEL;
	const provider = String(params.provider ?? "").trim() || defaultProvider;
	const model = String(params.model ?? "").trim() || defaultModel;
	const aliasIndex = buildModelAliasIndex({
		cfg: params.cfg ?? {},
		defaultProvider
	});
	const allowlist = buildConfiguredAllowlistKeys({
		cfg: params.cfg,
		defaultProvider
	});
	const seen = /* @__PURE__ */ new Set();
	const candidates = [];
	const addCandidate = (candidate, enforceAllowlist) => {
		if (!candidate.provider || !candidate.model) return;
		const key = modelKey(candidate.provider, candidate.model);
		if (seen.has(key)) return;
		if (enforceAllowlist && allowlist && !allowlist.has(key)) return;
		seen.add(key);
		candidates.push(candidate);
	};
	addCandidate({
		provider,
		model
	}, false);
	const modelFallbacks = (() => {
		if (params.fallbacksOverride !== void 0) return params.fallbacksOverride;
		const model = params.cfg?.agents?.defaults?.model;
		if (model && typeof model === "object") return model.fallbacks ?? [];
		return [];
	})();
	for (const raw of modelFallbacks) {
		const resolved = resolveModelRefFromString({
			raw: String(raw ?? ""),
			defaultProvider,
			aliasIndex
		});
		if (!resolved) continue;
		addCandidate(resolved.ref, true);
	}
	if (params.fallbacksOverride === void 0 && primary?.provider && primary.model) addCandidate({
		provider: primary.provider,
		model: primary.model
	}, false);
	return candidates;
}
async function runWithModelFallback(params) {
	const candidates = resolveFallbackCandidates({
		cfg: params.cfg,
		provider: params.provider,
		model: params.model,
		fallbacksOverride: params.fallbacksOverride
	});
	const authStore = params.cfg ? ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }) : null;
	const attempts = [];
	let lastError;
	for (let i = 0; i < candidates.length; i += 1) {
		const candidate = candidates[i];
		if (authStore) {
			const profileIds = resolveAuthProfileOrder({
				cfg: params.cfg,
				store: authStore,
				provider: candidate.provider
			});
			const isAnyProfileAvailable = profileIds.some((id) => !isProfileInCooldown(authStore, id));
			if (profileIds.length > 0 && !isAnyProfileAvailable) {
				attempts.push({
					provider: candidate.provider,
					model: candidate.model,
					error: `Provider ${candidate.provider} is in cooldown (all profiles unavailable)`,
					reason: "rate_limit"
				});
				continue;
			}
		}
		try {
			return {
				result: await params.run(candidate.provider, candidate.model),
				provider: candidate.provider,
				model: candidate.model,
				attempts
			};
		} catch (err) {
			if (shouldRethrowAbort(err)) throw err;
			const normalized = coerceToFailoverError(err, {
				provider: candidate.provider,
				model: candidate.model
			}) ?? err;
			if (!isFailoverError(normalized)) throw err;
			lastError = normalized;

[38908 more lines in file. Use offset=29580 to continue.]


This shows how fallback candidates are resolved. But the actual API format issue is happening when the model is resolved. Let me look at where the actual model resolution happens when running the agent. The `params.run(candidate.provider, candidate.model)` call eventually leads to running the model.

Let me look at `runEmbeddedPiAgent` or where the model API format is actually determined. The issue is that when falling back to zai, the model's API format is being incorrectly set to `openai-completions` instead of `anthropic-messages`.

Let me look at how the model is resolved in the embedded PI runner.

exec

grep -n "runEmbeddedPiAgent\|function runEmbeddedPiAgent" /usr/lib/node_modules/openclaw/dist/extensionAPI.js 2>/dev/null | head -10

grep -n "api.*=\|\.api" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -40


38188:				return runEmbeddedPiAgent({
38545:				return runEmbeddedPiAgent({
38906:						return runEmbeddedPiAgent({
67975:async function runEmbeddedPiAgent(params) {
67995:		if (workspaceResolution.usedFallback) log$2.warn(`[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`);
68487:export { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureAgentWorkspace, loadSessionStore, resolveAgentDir, resolveAgentIdentity, resolveAgentTimeoutMs, resolveAgentWorkspaceDir, resolveSessionFilePath, resolveStorePath, resolveThinkingDefault, runEmbeddedPiAgent, saveSessionStore };


881:	return model.api === "openai-completions";
963:			api: model.api ?? entry?.api
1014:				api: providerCfg?.api ?? "openai-responses",
1152:			apiKey: raw.elevenlabs?.apiKey,
1168:			apiKey: raw.openai?.apiKey,
1324:	if (provider === "elevenlabs") return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY;
1325:	if (provider === "openai") return config.openai.apiKey || process.env.OPENAI_API_KEY;
1673:	const apiKey = requireApiKey(await getApiKeyForModel({
1717:	const { text, apiKey, baseUrl, voiceId, modelId, outputFormat, seed, applyTextNormalization, languageCode, voiceSettings, timeoutMs } = params;
1758:	const { text, apiKey, model, voice, responseFormat, timeoutMs } = params;
1884:			const apiKey = resolveTtsApiKey(config, provider);
1965:			const apiKey = resolveTtsApiKey(config, provider);
11421:	if (!headers.has("authorization")) headers.set("authorization", `Token ${params.apiKey}`);
11475:	if (!headers.has("x-goog-api-key")) headers.set("x-goog-api-key", params.apiKey);
11526:	if (!headers.has("x-goog-api-key")) headers.set("x-goog-api-key", params.apiKey);
11592:	if (!headers.has("authorization")) headers.set("authorization", `Bearer ${params.apiKey}`);
11943:	if (!canvasModulePromise) canvasModulePromise = import("@napi-rs/canvas").catch((err) => {
13128:		const apiKey = requireApiKey(await resolveApiKeyForProvider({
13179:	const apiKey = requireApiKey(await resolveApiKeyForProvider({
14408:		label: envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth") ? "OAuth (env)" : maskApiKey(envKey.apiKey),
14457:	const api = entry?.api?.trim();
15088:		const apiSuffix = endpoint.api ? ` api: ${endpoint.api}` : "";
21662:	const showCost = authLabelValue === "api-key" || authLabelValue === "mixed";
23443:	if (envResolved?.apiKey) return envResolved.apiKey;
23448:	const apiProfile = [...listProfilesForProvider(store, "zai"), ...listProfilesForProvider(store, "z-ai")].find((id) => store.profiles[id]?.type === "api_key");
23466:	if (envResolved?.apiKey) return envResolved.apiKey;
23470:	const apiProfile = listProfilesForProvider(store, "minimax").find((id) => {
23472:		return cred?.type === "api_key" || cred?.type === "token";
23483:	if (envResolved?.apiKey) return envResolved.apiKey;
23487:	const apiProfile = listProfilesForProvider(store, "xiaomi").find((id) => {
23489:		return cred?.type === "api_key" || cred?.type === "token";
23516:			if (!resolved?.apiKey) continue;
23517:			let token = resolved.apiKey;
23518:			if (params.provider === "google-gemini-cli" || params.provider === "google-antigravity") token = parseGoogleToken(resolved.apiKey)?.token ?? resolved.apiKey;
23554:			const apiKey = resolveZaiApiKey();
23562:			const apiKey = resolveMinimaxApiKey();
23570:			const apiKey = resolveXiaomiApiKey();
24516:	if (envKey?.apiKey) {
24518:		return `api-key ${formatApiKeySnippet$1(envKey.apiKey)} (${envKey.source})`;
29496:	if (resolveEnvApiKey(params.provider)?.apiKey) return true;


Let me look at the pi-embedded file more closely to see where the model API is determined.


			id: trimmedModelId,
			name: trimmedModelId
		});
	}
}
function buildInlineProviderModels(providers) {
	return Object.entries(providers).flatMap(([providerId, entry]) => {
		const trimmed = providerId.trim();
		if (!trimmed) return [];
		return (entry?.models ?? []).map((model) => ({
			...model,
			provider: trimmed,
			baseUrl: entry?.baseUrl,
			api: model.api ?? entry?.api
		}));
	});
}
function buildModelAliasLines$1(cfg) {
	const models = cfg?.agents?.defaults?.models ?? {};
	const entries = [];
	for (const [keyRaw, entryRaw] of Object.entries(models)) {
		const model = String(keyRaw ?? "").trim();
		if (!model) continue;
		const alias = String(entryRaw?.alias ?? "").trim();
		if (!alias) continue;
		entries.push({
			alias,
			model
		});
	}
	return entries.toSorted((a, b) => a.alias.localeCompare(b.alias)).map((entry) => `- ${entry.alias}: ${entry.model}`);
}
function resolveModel$4(provider, modelId, agentDir, cfg) {
	const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
	const authStorage = discoverAuthStorage(resolvedAgentDir);
	const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
	const model = modelRegistry.find(provider, modelId);
	if (!model) {
		const providers = cfg?.models?.providers ?? {};
		const inlineModels = buildInlineProviderModels(providers);
		const normalizedProvider = normalizeProviderId(provider);
		const inlineMatch = inlineModels.find((entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId);
		if (inlineMatch) return {
			model: normalizeModelCompat(inlineMatch),
			authStorage,
			modelRegistry
		};
		const codexForwardCompat = resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry);
		if (codexForwardCompat) return {
			model: codexForwardCompat,
			authStorage,
			modelRegistry
		};
		const anthropicForwardCompat = resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry);
		if (anthropicForwardCompat) return {
			model: anthropicForwardCompat,
			authStorage,
			modelRegistry
		};
		const providerCfg = providers[provider];
		if (providerCfg || modelId.startsWith("mock-")) return {
			model: normalizeModelCompat({
				id: modelId,
				name: modelId,
				api: providerCfg?.api ?? "openai-responses",
				provider,
				baseUrl: providerCfg?.baseUrl,
				reasoning: false,
				input: ["text"],
				cost: {
					input: 0,
					output: 0,
					cacheRead: 0,
					cacheWrite: 0
				},
				contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
				maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS
			}),
			authStorage,
			modelRegistry
		};
		return {
			error: `Unknown model: ${provider}/${modelId}`,
			authStorage,
			modelRegistry
		};
	}
	return {
		model: normalizeModelCompat(model),
		authStorage,
		modelRegistry
	};
}

//#endregion
//#region src/media/audio.ts
const VOICE_AUDIO_EXTENSIONS = new Set([
	".oga",
	".ogg",
	".opus"

[67302 more lines in file. Use offset=1050 to continue.]


Now I see the issue clearly! Looking at the code:

```javascript
const providerCfg = providers[provider];
if (providerCfg || modelId.startsWith("mock-")) return {
	model: normalizeModelCompat({
		id: modelId,
		name: modelId,
		api: providerCfg?.api ?? "openai-responses",  // <-- Line 1014
```

The bug is on line 1014. When a model is not found in the registry, it tries to use the provider config's API (`providerCfg?.api`). However, looking at the user's config:

```json
"zai": {
	"baseUrl": "https://api.z.ai/api/anthropic",
	"apiKey": "...",
	"api": "anthropic-messages",
	...
}
```

The provider config does have `"api": "anthropic-messages"`. So `providerCfg?.api` should be `"anthropic-messages"`.

But wait, the session logs show `"api": "openai-completions"`, not `"openai-responses"`. Let me check if there's something else going on.

Let me check where the model resolution happens during the actual run. Maybe there's a different code path being taken.

Actually, wait. Let me look more carefully at the config. The model in the session is `"model": "glm-4.7"` but the config has `"id": "GLM-4.7"` (uppercase). This could be a case-sensitivity issue where the model isn't being found in the registry, and then the fallback logic isn't working correctly.

Let me check how the model registry works and how models are discovered.

grep -n "discoverModels\|modelRegistry\|registry" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -30

grep -n "find\|normalizeModelId" /usr/lib/node_modules/openclaw/dist/model-registry-*.js 2>/dev/null | head -30


13:import { n as discoverModels, t as discoverAuthStorage } from "./pi-model-discovery-DzFOAbQt.js";
903:function resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) {
909:		const template = modelRegistry.find(normalizedProvider, templateId);
935:function resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) {
946:		const template = modelRegistry.find(normalizedProvider, templateId);
985:	const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
986:	const model = modelRegistry.find(provider, modelId);
995:			modelRegistry
997:		const codexForwardCompat = resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry);
1001:			modelRegistry
1003:		const anthropicForwardCompat = resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry);
1007:			modelRegistry
1029:			modelRegistry
1034:			modelRegistry
1040:		modelRegistry
2264:let registryLocked = false;
2268:* These are built-in commands from commands-registry.data.ts.
2315:	if (registryLocked) return {
2413:	registryLocked = true;
2423:		registryLocked = false;
2452:function getHooksForName(registry, hookName) {
2453:	return registry.typedHooks.filter((h) => h.hookName === hookName).toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
2456:* Create a hook runner for a specific registry.
2458:function createHookRunner(registry, options = {}) {
2466:		const hooks = getHooksForName(registry, hookName);
2485:		const hooks = getHooksForName(registry, hookName);
2586:		const hooks = getHooksForName(registry, "tool_result_persist");
2643:		return registry.typedHooks.some((h) => h.hookName === hookName);
2649:		return registry.typedHooks.filter((h) => h.hookName === hookName).length;
2677:* Initialize the global hook runner with a plugin registry.


(no output)


Let me check the model discovery and registry files.

ls /usr/lib/node_modules/openclaw/dist/ | grep -i model

grep -n "find\|modelId" /usr/lib/node_modules/openclaw/dist/pi-model-discovery-DzFOAbQt.js 2>/dev/null | head -40


model-auth-Dz75Htwr.js
model-selection-BG3SCK40.js
model-selection-D3KJIAFQ.js
models-cli-BTX6eflC.js
models-cli-Dxx7UZWi.js
pi-model-discovery-CV2V1HHz.js
pi-model-discovery-DzEIEgHL.js
pi-model-discovery-DzFOAbQt.js
pi-model-discovery-EhM2JAQo.js


Let me look at the model discovery file to understand how models are looked up.


import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import path from "node:path";
import { AuthStorage, AuthStorage as AuthStorage$1, ModelRegistry, ModelRegistry as ModelRegistry$1 } from "@mariozechner/pi-coding-agent";

//#region src/agents/pi-model-discovery.ts
var pi_model_discovery_exports = /* @__PURE__ */ __exportAll({
	AuthStorage: () => AuthStorage$1,
	ModelRegistry: () => ModelRegistry$1,
	discoverAuthStorage: () => discoverAuthStorage,
	discoverModels: () => discoverModels
});
function discoverAuthStorage(agentDir) {
	return new AuthStorage(path.join(agentDir, "auth.json"));
}
function discoverModels(authStorage, agentDir) {
	return new ModelRegistry(authStorage, path.join(agentDir, "models.json"));
}

//#endregion
export { discoverModels as n, pi_model_discovery_exports as r, discoverAuthStorage as t };


The `ModelRegistry` is imported from `@mariozechner/pi-coding-agent`. This is an external package. Let me check what models are registered there and how the `find` method works.

Actually, I think the issue might be simpler. Let me look at what happens when the model ID doesn't match exactly. Looking at the session log, the model is `"glm-4.7"` (lowercase) but the config has `"GLM-4.7"` (uppercase). Let me check if there's case-insensitive matching happening.

Actually wait, let me re-read the config:

```json
"models": [
  {
	"id": "GLM-4.7",
	"name": "GLM 4.7",
	...
  }
]
```

And the session log shows:
```json
"model": "glm-4.7"
```

The model ID in the request is lowercase `glm-4.7` but the config has uppercase `GLM-4.7`. This could be the issue - the model isn't being found because of case sensitivity, and then it's falling back to the default API format.

Let me also check how the model is being called. The user said Carl is configured to use the AI, but sent a message and got no response. Let me check how the model reference is being resolved when calling zai.

grep -rn "glm\|GLM" /usr/lib/node_modules/openclaw/dist/auth-profiles-*.js 2>/dev/null | head -20

grep -n "openai-completions" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null


621:		id: "hf:zai-org/GLM-4.7",
622:		name: "GLM-4.7",
733:		id: "hf:zai-org/GLM-4.5",
734:		name: "GLM-4.5",
741:		id: "hf:zai-org/GLM-4.6",
742:		name: "GLM-4.6",
929:		id: "zai-org-glm-4.7",
930:		name: "GLM 4.7",


881:	return model.api === "openai-completions";
63775:	"openai-completions",
64955:* Only applies to Anthropic provider (OpenRouter uses openai-completions API


I see! There are hardcoded GLM models in the auth-profiles file. Let me check those entries:


		reasoning: true,
		input: ["text"],
		contextWindow: 256e3,
		maxTokens: 8192
	},
	{
		id: "hf:zai-org/GLM-4.7",
		name: "GLM-4.7",
		reasoning: false,
		input: ["text"],
		contextWindow: 198e3,
		maxTokens: 128e3
	},
	{
		id: "hf:deepseek-ai/DeepSeek-R1-0528",
		name: "DeepSeek R1 0528",
		reasoning: false,
		input: ["text"],
		contextWindow: 128e3,
		maxTokens: 8192
	},
	{
		id: "hf:deepseek-ai/DeepSeek-V3-0324",
		name: "DeepSeek V3 0324",
		reasoning: false,
		input: ["text"],
		contextWindow: 128e3,
		maxTokens: 8192
	},
	{
		id: "hf:deepseek-ai/DeepSeek-V3.1",
		name: "DeepSeek V3.1",
		reasoning: false,
		input: ["text"],
		contextWindow: 128e3,
		maxTokens: 8192
	},
	{
		id: "hf:deepseek-ai/DeepSeek-V3.1-Terminus",
		name: "DeepSeek V3.1 Terminus",
		reasoning: false,
		input: ["text"],
		contextWindow: 128e3,
		maxTokens: 8192
	},
	{
		id: "hf:deepseek-ai/DeepSeek-V3.2",
		name: "DeepSeek V3.2",
		reasoning: false,
		input: ["text"],

[2344 more lines in file. Use offset=665 to continue.]


Let me check around line 929 where the zai-org-glm-4.7 model is defined:


		id: "openai-gpt-oss-120b",
		name: "OpenAI GPT OSS 120B",
		reasoning: false,
		input: ["text"],
		contextWindow: 131072,
		maxTokens: 8192,
		privacy: "private"
	},
	{
		id: "zai-org-glm-4.7",
		name: "GLM 4.7",
		reasoning: true,
		input: ["text"],
		contextWindow: 202752,
		maxTokens: 8192,
		privacy: "private"
	},
	{
		id: "claude-opus-45",
		name: "Claude Opus 4.5 (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 202752,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "claude-sonnet-45",
		name: "Claude Sonnet 4.5 (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 202752,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "openai-gpt-52",
		name: "GPT-5.2 (via Venice)",
		reasoning: true,
		input: ["text"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "openai-gpt-52-codex",
		name: "GPT-5.2 Codex (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "gemini-3-pro-preview",
		name: "Gemini 3 Pro (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 202752,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "gemini-3-flash-preview",
		name: "Gemini 3 Flash (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "grok-41-fast",
		name: "Grok 4.1 Fast (via Venice)",
		reasoning: true,
		input: ["text", "image"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "grok-code-fast-1",
		name: "Grok Code Fast 1 (via Venice)",
		reasoning: true,
		input: ["text"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "kimi-k2-thinking",
		name: "Kimi K2 Thinking (via Venice)",
		reasoning: true,
		input: ["text"],
		contextWindow: 262144,
		maxTokens: 8192,
		privacy: "anonymized"
	},
	{
		id: "minimax-m21",

[1989 more lines in file. Use offset=1020 to continue.]


Now I see the hardcoded models don't have an `api` field! The `zai-org-glm-4.7` model entry doesn't specify `api: "anthropic-messages"`. Let me check if there's a default API being applied elsewhere.

Actually, looking more carefully at the code flow:

1. When `resolveModel$4` is called with provider `zai` and model `glm-4.7` (lowercase)
2. The `modelRegistry.find(provider, modelId)` doesn't find the model because:
   - The hardcoded model has ID `zai-org-glm-4.7` (with prefix and different format)
   - The user's config has `GLM-4.7` (uppercase)
   - The request has `glm-4.7` (lowercase)
3. It falls back to building a model from the provider config
4. The provider config has `"api": "anthropic-messages"`
5. But the session log shows `"api": "openai-completions"`...

Wait, there must be something else going on. Let me check where the model API is being overwritten to `openai-completions`.

Actually, let me look at the model resolution in the session log more carefully. The log shows:
```json
"api": "openai-completions",
"provider": "zai",
"model": "glm-4.7"
```

And the stop reason is `"stop"`, which means the request completed without error, just returned empty content.

Let me check if there's any hardcoded logic that forces zai to use openai-completions format.

grep -rn "zai.*openai\|openai.*zai" /usr/lib/node_modules/openclaw/dist/*.js 2>/dev/null | head -20


/usr/lib/node_modules/openclaw/dist/config-guard-Ju243EuT.js:5061:	program.command("onboard").description("Interactive wizard to set up the gateway, workspace, and skills").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/onboard", "docs.openclaw.ai/cli/onboard")}\n`).option("--workspace <dir>", "Agent workspace directory (default: ~/.openclaw/workspace)").option("--reset", "Reset config + credentials + sessions + workspace before running wizard").option("--non-interactive", "Run without prompts", false).option("--accept-risk", "Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)", false).option("--flow <flow>", "Wizard flow: quickstart|advanced|manual").option("--mode <mode>", "Wizard mode: local|remote").option("--auth-choice <choice>", "Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|qianfan-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip").option("--token-provider <id>", "Token provider id (non-interactive; used with --auth-choice token)").option("--token <token>", "Token value (non-interactive; used with --auth-choice token)").option("--token-profile-id <id>", "Auth profile id (non-interactive; default: <provider>:manual)").option("--token-expires-in <duration>", "Optional token expiry duration (e.g. 365d, 12h)").option("--anthropic-api-key <key>", "Anthropic API key").option("--openai-api-key <key>", "OpenAI API key").option("--openrouter-api-key <key>", "OpenRouter API key").option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key").option("--cloudflare-ai-gateway-account-id <id>", "Cloudflare Account ID").option("--cloudflare-ai-gateway-gateway-id <id>", "Cloudflare AI Gateway ID").option("--cloudflare-ai-gateway-api-key <key>", "Cloudflare AI Gateway API key").option("--moonshot-api-key <key>", "Moonshot API key").option("--kimi-code-api-key <key>", "Kimi Coding API key").option("--gemini-api-key <key>", "Gemini API key").option("--zai-api-key <key>", "Z.AI API key").option("--xiaomi-api-key <key>", "Xiaomi API key").option("--minimax-api-key <key>", "MiniMax API key").option("--synthetic-api-key <key>", "Synthetic API key").option("--venice-api-key <key>", "Venice API key").option("--opencode-zen-api-key <key>", "OpenCode Zen API key").option("--xai-api-key <key>", "xAI API key").option("--qianfan-api-key <key>", "QIANFAN API key").option("--gateway-port <port>", "Gateway port").option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom").option("--gateway-auth <mode>", "Gateway auth: token|password").option("--gateway-token <token>", "Gateway token (token auth)").option("--gateway-password <password>", "Gateway password (password auth)").option("--remote-url <url>", "Remote Gateway WebSocket URL").option("--remote-token <token>", "Remote Gateway token (optional)").option("--tailscale <mode>", "Tailscale: off|serve|funnel").option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit").option("--install-daemon", "Install gateway service").option("--no-install-daemon", "Skip gateway service install").option("--skip-daemon", "Skip gateway service install").option("--daemon-runtime <runtime>", "Daemon runtime: node|bun").option("--skip-channels", "Skip channel setup").option("--skip-skills", "Skip skills setup").option("--skip-health", "Skip health check").option("--skip-ui", "Skip Control UI/TUI prompts").option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun").option("--json", "Output JSON summary", false).action(async (opts, command) => {
/usr/lib/node_modules/openclaw/dist/index.js:5127:	program.command("onboard").description("Interactive wizard to set up the gateway, workspace, and skills").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/onboard", "docs.openclaw.ai/cli/onboard")}\n`).option("--workspace <dir>", "Agent workspace directory (default: ~/.openclaw/workspace)").option("--reset", "Reset config + credentials + sessions + workspace before running wizard").option("--non-interactive", "Run without prompts", false).option("--accept-risk", "Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)", false).option("--flow <flow>", "Wizard flow: quickstart|advanced|manual").option("--mode <mode>", "Wizard mode: local|remote").option("--auth-choice <choice>", "Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|qianfan-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip").option("--token-provider <id>", "Token provider id (non-interactive; used with --auth-choice token)").option("--token <token>", "Token value (non-interactive; used with --auth-choice token)").option("--token-profile-id <id>", "Auth profile id (non-interactive; default: <provider>:manual)").option("--token-expires-in <duration>", "Optional token expiry duration (e.g. 365d, 12h)").option("--anthropic-api-key <key>", "Anthropic API key").option("--openai-api-key <key>", "OpenAI API key").option("--openrouter-api-key <key>", "OpenRouter API key").option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key").option("--cloudflare-ai-gateway-account-id <id>", "Cloudflare Account ID").option("--cloudflare-ai-gateway-gateway-id <id>", "Cloudflare AI Gateway ID").option("--cloudflare-ai-gateway-api-key <key>", "Cloudflare AI Gateway API key").option("--moonshot-api-key <key>", "Moonshot API key").option("--kimi-code-api-key <key>", "Kimi Coding API key").option("--gemini-api-key <key>", "Gemini API key").option("--zai-api-key <key>", "Z.AI API key").option("--xiaomi-api-key <key>", "Xiaomi API key").option("--minimax-api-key <key>", "MiniMax API key").option("--synthetic-api-key <key>", "Synthetic API key").option("--venice-api-key <key>", "Venice API key").option("--opencode-zen-api-key <key>", "OpenCode Zen API key").option("--xai-api-key <key>", "xAI API key").option("--qianfan-api-key <key>", "QIANFAN API key").option("--gateway-port <port>", "Gateway port").option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom").option("--gateway-auth <mode>", "Gateway auth: token|password").option("--gateway-token <token>", "Gateway token (token auth)").option("--gateway-password <password>", "Gateway password (password auth)").option("--remote-url <url>", "Remote Gateway WebSocket URL").option("--remote-token <token>", "Remote Gateway token (optional)").option("--tailscale <mode>", "Tailscale: off|serve|funnel").option("--tailscale-reset-on-exit", "Reset tailscale serve/funnel on exit").option("--install-daemon", "Install gateway service").option("--no-install-daemon", "Skip gateway service install").option("--skip-daemon", "Skip gateway service install").option("--daemon-runtime <runtime>", "Daemon runtime: node|bun").option("--skip-channels", "Skip channel setup").option("--skip-skills", "Skip skills setup").option("--skip-health", "Skip health check").option("--skip-ui", "Skip Control UI/TUI prompts").option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun").option("--json", "Output JSON summary", false).action(async (opts, command) => {


function isOpenAiCompletionsModel(model) {
	return model.api === "openai-completions";
}
function normalizeModelCompat(model) {
	const baseUrl = model.baseUrl ?? "";
	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
	const openaiModel = model;
	const compat = openaiModel.compat ?? void 0;
	if (compat?.supportsDeveloperRole === false) return model;
	openaiModel.compat = compat ? {
		...compat,
		supportsDeveloperRole: false
	} : { supportsDeveloperRole: false };
	return openaiModel;
}

//#endregion
//#region src/agents/pi-embedded-runner/model.ts
const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex";
const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"];
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"];
function resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) {
	const normalizedProvider = normalizeProviderId(provider);
	const trimmedModelId = modelId.trim();
	if (normalizedProvider !== "openai-codex") return;
	if (trimmedModelId.toLowerCase() !== OPENAI_CODEX_GPT_53_MODEL_ID) return;
	for (const templateId of OPENAI_CODEX_TEMPLATE_MODEL_IDS) {
		const template = modelRegistry.find(normalizedProvider, templateId);

[67508 more lines in file. Use offset=980 to continue.]


I see! Look at the `normalizeModelCompat` function:

```javascript
function normalizeModelCompat(model) {
	const baseUrl = model.baseUrl ?? "";
	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
	...
}
```

This function only does something special for zai models IF they are already `openai-completions` models. It doesn't fix the case where the API format is wrong.

The problem is clear now. When the model is not found in the registry and is built from the provider config, if `providerCfg?.api` is undefined or wrong, it defaults to `"openai-responses"`.

But wait, the session log shows `"api": "openai-completions"` not `"openai-responses"`. Let me check if there's another place where the API is being set.

Actually, let me trace through more carefully. The code path when a model is not found:

1. `resolveModel$4` is called
2. `modelRegistry.find(provider, modelId)` returns `null`
3. `buildInlineProviderModels` is called but may not find a match
4. Forward compat models don't match
5. Falls back to building from provider config:
```javascript
const providerCfg = providers[provider];
if (providerCfg || modelId.startsWith("mock-")) return {
	model: normalizeModelCompat({
		id: modelId,
		name: modelId,
		api: providerCfg?.api ?? "openai-responses",
		provider,
		...
	})
}
```

The user's config has `"api": "anthropic-messages"` in the zai provider config, so `providerCfg?.api` should be `"anthropic-messages"`.

Unless... the provider ID is being normalized differently.

I'll investigate if the provider name transformation is causing the configuration lookup to fail silently, potentially defaulting to an unexpected API type.

grep -n "normalizeProviderId" /usr/lib/node_modules/openclaw/dist/extensionAPI.js 2>/dev/null | head -10

grep -n "function normalizeProviderId" /usr/lib/node_modules/openclaw/dist/extensionAPI.js 2>/dev/null


4:import { B as resolveOpenClawAgentDir, C as getShellPathFromLoginShell, F as resolveApiKeyForProfile, G as DEFAULT_PROVIDER, H as resolveAuthProfileDisplayLabel, I as listProfilesForProvider, L as markAuthProfileGood, M as isProfileInCooldown, N as markAuthProfileFailure, O as isTruthyEnvValue, P as markAuthProfileUsed, R as ensureAuthProfileStore, S as resolveModelAuthMode, T as resolveShellEnvFallbackTimeoutMs, U as DEFAULT_CONTEXT_TOKENS, V as normalizeSecretInput, W as DEFAULT_MODEL, _ as getApiKeyForModel, a as modelKey, b as resolveApiKeyForProvider, c as resolveConfiguredModelRef, d as resolveThinkingDefault, f as normalizeGoogleModelId, i as isCliProvider, j as resolveAuthProfileOrder, k as parseBooleanValue$1, l as resolveDefaultModelForAgent, n as buildConfiguredAllowlistKeys, o as normalizeProviderId, r as buildModelAliasIndex, t as buildAllowedModelSet, u as resolveModelRefFromString, v as getCustomProviderApiKey, x as resolveEnvApiKey, y as requireApiKey, z as resolveAuthStorePathForDisplay } from "./model-selection-BG3SCK40.js";
974:	const normalizedProvider = normalizeProviderId(provider);
1006:	const normalizedProvider = normalizeProviderId(provider);
1060:		const normalizedProvider = normalizeProviderId(provider);
1061:		const inlineMatch = inlineModels.find((entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId);
11824:	const normalized = normalizeProviderId(id);
14294:		const key = normalizeProviderId(p);
14374:		provider: provider ? normalizeProviderId(provider) : void 0,
14488:	const providerKey = normalizeProviderId(provider);
14492:		for (const [key, value] of Object.entries(map)) if (normalizeProviderId(key) === providerKey) return value;


Let me look at the `normalizeProviderId` function in the model-selection file:

grep -n "normalizeProviderId" /usr/lib/node_modules/openclaw/dist/model-selection-BG3SCK40.js 2>/dev/null | head -20


422:	const providerKey = normalizeProviderId(provider);
423:	return Object.entries(store.profiles).filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey).map(([id]) => id);
465:	const providerKey = normalizeProviderId(params.provider);
468:	if (legacyCfg && normalizeProviderId(legacyCfg.provider) === providerKey && legacyCfg.mode !== "oauth") return null;
492:	const providerKey = normalizeProviderId(params.provider);
841:		for (const [key, value] of Object.entries(map)) if (normalizeProviderId(key) === params.providerId) return value;
900:				providerId: normalizeProviderId(profile.provider)
921:		providerId: normalizeProviderId(store.profiles[profileId]?.provider ?? "")
941:	const providerKey = normalizeProviderId(provider);
946:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
951:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
954:	const explicitProfiles = cfg?.auth?.profiles ? Object.entries(cfg.auth.profiles).filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey).map(([profileId]) => profileId) : [];
960:		if (normalizeProviderId(cred.provider) !== providerKey) return false;
963:			if (normalizeProviderId(profileConfig.provider) !== providerKey) return false;
1371:	const normalized = normalizeProviderId(provider);
1372:	if (normalized === provider) return Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1];
1373:	return providers[normalized] ?? Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1];
1477:	const normalized = normalizeProviderId(provider);
1491:	const normalized = normalizeProviderId(provider);
1559:	if (authOverride === void 0 && normalizeProviderId(resolved) === "amazon-bedrock") return "aws-sdk";


Offset 11815 is beyond end of file (2754 lines total)


Let me search for the function definition:

grep -n "function.*normalizeProviderId\|const.*normalizeProviderId\|normalizeProviderId.*=" /usr/lib/node_modules/openclaw/dist/model-selection-BG3SCK40.js 2>/dev/null | head -10

grep -n "normalizeProviderId" /usr/lib/node_modules/openclaw/dist/model-selection-BG3SCK40.js 2>/dev/null | tail -20


422:	const providerKey = normalizeProviderId(provider);
423:	return Object.entries(store.profiles).filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey).map(([id]) => id);
465:	const providerKey = normalizeProviderId(params.provider);
468:	if (legacyCfg && normalizeProviderId(legacyCfg.provider) === providerKey && legacyCfg.mode !== "oauth") return null;
492:	const providerKey = normalizeProviderId(params.provider);
841:		for (const [key, value] of Object.entries(map)) if (normalizeProviderId(key) === params.providerId) return value;
941:	const providerKey = normalizeProviderId(provider);
946:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
951:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
954:	const explicitProfiles = cfg?.auth?.profiles ? Object.entries(cfg.auth.profiles).filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey).map(([profileId]) => profileId) : [];


921:		providerId: normalizeProviderId(store.profiles[profileId]?.provider ?? "")
941:	const providerKey = normalizeProviderId(provider);
946:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
951:		for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value;
954:	const explicitProfiles = cfg?.auth?.profiles ? Object.entries(cfg.auth.profiles).filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey).map(([profileId]) => profileId) : [];
960:		if (normalizeProviderId(cred.provider) !== providerKey) return false;
963:			if (normalizeProviderId(profileConfig.provider) !== providerKey) return false;
1371:	const normalized = normalizeProviderId(provider);
1372:	if (normalized === provider) return Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1];
1373:	return providers[normalized] ?? Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1];
1477:	const normalized = normalizeProviderId(provider);
1491:	const normalized = normalizeProviderId(provider);
1559:	if (authOverride === void 0 && normalizeProviderId(resolved) === "amazon-bedrock") return "aws-sdk";
2550:function normalizeProviderId(provider) {
2559:	const normalized = normalizeProviderId(provider);
2563:	return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
2580:		const provider = normalizeProviderId(defaultProvider);
2586:	const provider = normalizeProviderId(trimmed.slice(0, slash).trim());
2725:		const providerKey = normalizeProviderId(parsed.provider);
2754:export { auth_profiles_exports as A, resolveOpenClawAgentDir as B, getShellPathFromLoginShell as C, shouldEnableShellEnvFallback as D, shouldDeferShellEnvFallback as E, resolveApiKeyForProfile as F, DEFAULT_PROVIDER as G, resolveAuthProfileDisplayLabel as H, listProfilesForProvider as I, markAuthProfileGood as L, isProfileInCooldown as M, markAuthProfileFailure as N, isTruthyEnvValue as O, markAuthProfileUsed as P, ensureAuthProfileStore as R, resolveModelAuthMode as S, resolveShellEnvFallbackTimeoutMs as T, DEFAULT_CONTEXT_TOKENS as U, normalizeSecretInput as V, DEFAULT_MODEL as W, getApiKeyForModel as _, modelKey as a, resolveApiKeyForProvider as b, resolveConfiguredModelRef as c, resolveThinkingDefault as d, normalizeGoogleModelId as f, resolveImplicitProviders as g, resolveImplicitCopilotProvider as h, isCliProvider as i, resolveAuthProfileOrder as j, parseBooleanValue as k, resolveDefaultModelForAgent as l, resolveImplicitBedrockProvider as m, buildConfiguredAllowlistKeys as n, normalizeProviderId as o, normalizeProviders as p, buildModelAliasIndex as r, parseModelRef as s, buildAllowedModelSet as t, resolveModelRefFromString as u, getCustomProviderApiKey as v, loadShellEnvFallback as w, resolveEnvApiKey as x, requireApiKey as y, resolveAuthStorePathForDisplay as z };


Found it at line 2550! Let me look at the function:


	return `${provider}/${model}`;
}
function normalizeProviderId(provider) {
	const normalized = provider.trim().toLowerCase();
	if (normalized === "z.ai" || normalized === "z-ai") return "zai";
	if (normalized === "opencode-zen") return "opencode";
	if (normalized === "qwen") return "qwen-portal";
	if (normalized === "kimi-code") return "kimi-coding";
	return normalized;
}
function isCliProvider(provider, cfg) {
	const normalized = normalizeProviderId(provider);
	if (normalized === "claude-cli") return true;
	if (normalized === "codex-cli") return true;
	const backends = cfg?.agents?.defaults?.cliBackends ?? {};
	return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
}
function normalizeAnthropicModelId(model) {
	const trimmed = model.trim();
	if (!trimmed) return trimmed;

[187 more lines in file. Use offset=2568 to continue.]


I see `normalizeProviderId` converts `z.ai` or `z-ai` to `zai`. The config has `"zai"` as the provider key, so that should work fine.

Let me now look at what happens when the model resolution happens. I think the issue might be in how the model is being resolved from the inline models vs. the provider config. Let me trace through more carefully.

Actually, let me look at `buildInlineProviderModels`:```javascript
function buildInlineProviderModels(providers) {
	return Object.entries(providers).flatMap(([providerId, entry]) => {
		const trimmed = providerId.trim();
		if (!trimmed) return [];
		return (entry?.models ?? []).map((model) => ({
			...model,
			provider: trimmed,
			baseUrl: entry?.baseUrl,
			api: model.api ?? entry?.api
		}));
	});
}
```

This takes models from the provider config and adds the provider's `api` setting to each model if the model doesn't have its own. So if the user has:
```json
"zai": {
	"api": "anthropic-messages",
	"models": [{"id": "GLM-4.7", ...}]
}
```

Then `buildInlineProviderModels` should produce:```javascript
{
	id: "GLM-4.7",
	provider: "zai",
	api: "anthropic-messages",  // inherited from provider
	baseUrl: "https://api.z.ai/api/anthropic",
	...
}```

And when looking up `zai/glm-4.7`, it should find this model... unless the case-sensitivity is the issue. `GLM-4.7` !== `glm-4.7`.

Let me check if there's case-insensitive matching happening anywhere.

grep -n "toLowerCase\|toUpperCase\|case.*insensitive" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -20

grep -n "modelId.*trim\|trim.*modelId" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -10


162:	return `${kind}:${id}`.toLowerCase();
351:		const matchKey = Object.keys(accounts).find((key) => key.toLowerCase() === normalizedAccountId.toLowerCase());
802:	const trimmed = value.trim().toLowerCase();
807:	if (Array.isArray(capabilities)) return capabilities.some((entry) => String(entry).trim().toLowerCase() === "inlinebuttons") ? "all" : "off";
907:	if (trimmedModelId.toLowerCase() !== OPENAI_CODEX_GPT_53_MODEL_ID) return;
939:	const lower = trimmedModelId.toLowerCase();
1052:	const mime = opts.contentType?.toLowerCase();
1114:	const normalized = value.trim().toLowerCase();
1359:	const normalized = trimmed.toLowerCase();
1366:	const normalized = trimmed.toLowerCase();
1377:	const normalized = value.trim().toLowerCase();
1420:			const key = rawKey.toLowerCase();
1785:	const normalized = outputFormat.toLowerCase();
2116:	const normalized = provider.trim().toLowerCase();
2304:	const trimmed = name.trim().toLowerCase();
2328:	const key = `/${command.name.toLowerCase()}`;
2364:	const key = commandName.toLowerCase();
3160:	return input.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "collection";
3764:	const tokens = new Set(parsed.rest.toLowerCase().split(":").filter(Boolean));
4696:	const action = normalizeArgValue(values.action)?.toLowerCase();


905:	const trimmedModelId = modelId.trim();
938:	const trimmedModelId = modelId.trim();
12548:	const normalizedModelId = modelId.toLowerCase().trim();
13078:		const modelId = entry.model?.trim();
37092:	const trimmed = modelId.trim();
37366:	const modelId = (params.model ?? "default").trim() || "default";
62182:	const rawModelId = params.modelId?.trim().toLowerCase();
62765:	const modelId = params.modelId?.trim();
64619:	const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
67862:		const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;


I see that model IDs are sometimes lowercased. Let me look at how the inline models are matched:

From earlier:```javascript
const inlineMatch = inlineModels.find((entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId);
```

This uses `entry.id === modelId` - exact match! If the config has `"id": "GLM-4.7"` (uppercase) and the request has `"glm-4.7"` (lowercase), they won't match.

Let me verify this is the issue by checking the exact model IDs in the config and what's being requested.

Actually, I think I understand the issue now. Let me trace through the full flow:1. User config has:
   ```json   "zai": {
     "api": "anthropic-messages",
     "models": [{"id": "GLM-4.7", ...}]  // uppercase
   }   ```

2. Agent config references: `"zai/GLM-4.7"` (uppercase)

3. But somewhere in the session/fallback chain, it gets normalized to: `"zai/glm-4.7"` (lowercase)

4. `buildInlineProviderModels` produces models with `id: "GLM-4.7"` (uppercase from config)

5. Looking for `glm-4.7` (lowercase) doesn't match `GLM-4.7` (uppercase)6. Falls back to building from provider config:
   ```javascript
   const providerCfg = providers[provider];  // "zai"
   // providerCfg.api should be "anthropic-messages"
   ```7. But the session shows `"api": "openai-completions"`, not `"anthropic-messages"`...

I'm detecting a potential discrepancy in the API configuration. The system seems to be defaulting to a different API format than expected, which could indicate a deeper configuration or resolution problem.

cat /home/crogers2287/.openclaw/openclaw.json | jq '.models.providers.zai'


GLM 4.7

Input: text

Input: text


The config looks correct. Now let me check what model is actually being used in the session. Let me look at the full session entry for Carl's thread.

exec

cat /home/crogers2287/.openclaw/sessions/1014b26a-3847-4349-a397-031f195799a2.jsonl 2>/dev/null | head -50


(no output)


The session file doesn't exist. Let me check what sessions exist for Carl and look at the one that has the zai issue.


Cron: Deal Post - X + Pinterest (every 30 min)

Cron: Canna: Morning X Deal Post (8 AM EST)


I see it! Look at this session:

```json
{
  "key": "agent:ash-permits:main:thread:11491",
  ...
  "sessionId": "1014b26a-3847-4349-a397-031f195799a2",
  "model": "glm-4.7",  // <-- lowercase, no provider prefix!
  ...
}
```

The session shows `model: "glm-4.7"` but NOT `zai/glm-4.7`. This means the provider isn't being set correctly. The model ID is being stored without the provider prefix.

Looking at the session history I retrieved earlier:
```json
{
  "role": "assistant",
  "content": [],
  "api": "openai-completions",
  "provider": "zai",
  "model": "glm-4.7",
  ...
}
```

So the provider is being set to `zai` but the API format is `openai-completions`.

The issue is: when the model is resolved from the fallback chain, if it's not found in the registry, the code creates a synthetic model from the provider config. But something is setting the API to `openai-completions` instead of using the provider config's `api` setting.

Let me look at where the model resolution happens in the embedded PI runner and trace through the code path more carefully. I think the issue might be in how the model is being resolved during the fallback chain.

Actually, let me check if there's a hardcoded default for certain providers:

grep -rn "openai-completions\|openai-responses" /usr/lib/node_modules/openclaw/dist/entry.js 2>/dev/null | head -20

grep -n "default.*api\|api.*default" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -20


Let me look at how the model is being resolved in the actual PI runner. I need to find where the API format is being set incorrectly.

grep -n "resolveModel\|buildModel" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -20

grep -n "providerCfg" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null


9:import { C as normalizeProviderId, D as resolveModelRefFromString, E as resolveDefaultModelForAgent, F as DEFAULT_MODEL, I as DEFAULT_PROVIDER, L as resolveAuthProfileDisplayLabel, O as resolveThinkingDefault, P as DEFAULT_CONTEXT_TOKENS, R as normalizeSecretInput, S as modelKey, T as resolveConfiguredModelRef, V as resolveShellEnvFallbackTimeoutMs, W as isTruthyEnvValue, _ as resolveOpenClawAgentDir, a as resolveEnvApiKey, b as buildModelAliasIndex, c as resolveAuthProfileOrder, d as markAuthProfileUsed, f as resolveApiKeyForProfile, g as resolveAuthStorePathForDisplay, h as ensureAuthProfileStore, i as resolveApiKeyForProvider, k as normalizeGoogleModelId, l as isProfileInCooldown, m as markAuthProfileGood, n as getCustomProviderApiKey, o as resolveModelAuthMode, p as listProfilesForProvider, r as requireApiKey, t as getApiKeyForModel, u as markAuthProfileFailure, v as buildAllowedModelSet, x as isCliProvider, y as buildConfiguredAllowlistKeys, z as getShellPathFromLoginShell } from "./model-auth-Dz75Htwr.js";
967:function buildModelAliasLines$1(cfg) {
982:function resolveModel$4(provider, modelId, agentDir, cfg) {
1117:function resolveModelOverridePolicy(overrides) {
1150:		modelOverrides: resolveModelOverridePolicy(raw.modelOverrides),
1645:	const aliasIndex = buildModelAliasIndex({
1649:	const resolved = resolveModelRefFromString({
1671:	const resolved = resolveModel$4(ref.provider, ref.model, void 0, cfg);
9603:function resolveModelCost(params) {
9718:	const costConfig = resolveModelCost({
11405:function resolveModel$3(model) {
11412:	const model = resolveModel$3(params.model);
11459:function resolveModel$2(model) {
11471:	const model = resolveModel$2(params.model);
11510:function resolveModel$1(model) {
11522:	const model = resolveModel$1(params.model);
11574:function resolveModel(model) {
11582:	const model = resolveModel(params.model);
11748:function resolveModelEntries(params) {
13023:function buildModelDecision(params) {


1009:		const providerCfg = providers[provider];
1010:		if (providerCfg || modelId.startsWith("mock-")) return {
1014:				api: providerCfg?.api ?? "openai-responses",
1016:				baseUrl: providerCfg?.baseUrl,
1025:				contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
1026:				maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS
15972:	const providerCfg = cfg[providerKey];
15973:	if (!providerCfg || typeof providerCfg !== "object") return;
15975:	const typed = providerCfg;


Line 1014 shows `api: providerCfg?.api ?? "openai-responses"`. So if `providerCfg` is undefined or doesn't have an `api` property, it defaults to `"openai-responses"`. But wait, the session shows `"api": "openai-completions"`, not `"openai-responses"`.

Let me look at the code around line 1009 more carefully and understand when this code path is taken.


			model: anthropicForwardCompat,
			authStorage,
			modelRegistry
		};
		const providerCfg = providers[provider];
		if (providerCfg || modelId.startsWith("mock-")) return {
			model: normalizeModelCompat({
				id: modelId,
				name: modelId,
				api: providerCfg?.api ?? "openai-responses",
				provider,
				baseUrl: providerCfg?.baseUrl,
				reasoning: false,
				input: ["text"],
				cost: {
					input: 0,
					output: 0,
					cacheRead: 0,
					cacheWrite: 0
				},
				contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
				maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS
			}),
			authStorage,
			modelRegistry
		};
		return {
			error: `Unknown model: ${provider}/${modelId}`,
			authStorage,
			modelRegistry
		};
	}
	return {
		model: normalizeModelCompat(model),
		authStorage,
		modelRegistry
	};
}

//#endregion

[67307 more lines in file. Use offset=1045 to continue.]


I see the issue now. When the model is not found in the registry and needs to be built from the provider config, it uses `providerCfg?.api ?? "openai-responses"`. The user's config has `"api": "anthropic-messages"`, so this should work.

But wait - the session shows `"api": "openai-completions"`, not `"openai-responses"`. And the model shows `"provider": "zai"` which is correct.

Let me check if there's something converting `"openai-responses"` to `"openai-completions"` somewhere, or if there's another code path being taken.

Actually, let me look at the `normalizeModelCompat` function again - it might be modifying the API format.


	}
}

//#endregion
//#region src/agents/model-compat.ts
function isOpenAiCompletionsModel(model) {
	return model.api === "openai-completions";
}
function normalizeModelCompat(model) {
	const baseUrl = model.baseUrl ?? "";
	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
	const openaiModel = model;
	const compat = openaiModel.compat ?? void 0;
	if (compat?.supportsDeveloperRole === false) return model;
	openaiModel.compat = compat ? {
		...compat,
		supportsDeveloperRole: false
	} : { supportsDeveloperRole: false };
	return openaiModel;
}

[67457 more lines in file. Use offset=895 to continue.]


The `normalizeModelCompat` function only does something special for zai models IF they already have `api === "openai-completions"`. It doesn't change the API format.

So the question remains: where is `"openai-completions"` coming from?

Let me check if there's another place where models are resolved. Let me look at the `model-auth` file which is imported:

grep -n "api.*openai\|openai.*api" /usr/lib/node_modules/openclaw/dist/model-auth-Dz75Htwr.js 2>/dev/null | head -20


1065:		api: "openai-completions",
1103:		api: "openai-completions",
1118:		api: "openai-completions",
1163:		api: "openai-completions",
1170:		api: "openai-completions",
1177:		api: "openai-completions",


import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { o as resolveOAuthPath, s as resolveStateDir } from "./paths-BZtyHNCi.js";
import { n as DEFAULT_AGENT_ID } from "./session-key-nXYQSv-a.js";
import { p as resolveUserPath } from "./utils-4FS48tZP.js";
import { l as createSubsystemLogger } from "./exec-kbIbbKiD.js";
import { a as resolveAgentModelPrimary } from "./agent-scope-DdQkOxl9.js";
import { t as parseBooleanValue } from "./boolean-M-esQJt6.js";
import { t as formatCliCommand } from "./command-format-BUxhT1xL.js";
import { a as saveJsonFile, i as loadJsonFile, r as resolveCopilotApiToken, t as DEFAULT_COPILOT_API_BASE_URL } from "./github-copilot-token-C9IJh2Pn.js";
import path from "node:path";
import fs from "node:fs";
import { execFileSync, execSync } from "node:child_process";
import { getEnvApiKey, getOAuthApiKey, getOAuthProviders } from "@mariozechner/pi-ai";
import { BedrockClient, ListFoundationModelsCommand } from "@aws-sdk/client-bedrock";
import lockfile from "proper-lockfile";
import { createHash, randomBytes } from "node:crypto";

//#region src/infra/env.ts
const log$2 = createSubsystemLogger("env");
function isTruthyEnvValue(value) {
	return parseBooleanValue(value) === true;
}

//#endregion
//#region src/infra/shell-env.ts
const DEFAULT_TIMEOUT_MS = 15e3;
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
let lastAppliedKeys = [];
let cachedShellPath;
function resolveShell(env) {
	const shell = env.SHELL?.trim();
	return shell && shell.length > 0 ? shell : "/bin/sh";
}
function parseShellEnv(stdout) {
	const shellEnv = /* @__PURE__ */ new Map();
	const parts = stdout.toString("utf8").split("\0");
	for (const part of parts) {
		if (!part) continue;
		const eq = part.indexOf("=");
		if (eq <= 0) continue;
		const key = part.slice(0, eq);
		const value = part.slice(eq + 1);
		if (!key) continue;
		shellEnv.set(key, value);
	}
	return shellEnv;
}
function loadShellEnvFallback(opts) {
	const logger = opts.logger ?? console;
	const exec = opts.exec ?? execFileSync;

[2678 more lines in file. Use offset=51 to continue.]


Let me look at those lines with `openai-completions`:


	return mutated ? next : providers;
}
function buildMinimaxProvider() {
	return {
		baseUrl: MINIMAX_API_BASE_URL,
		api: "openai-completions",
		models: [{
			id: MINIMAX_DEFAULT_MODEL_ID,
			name: "MiniMax M2.1",
			reasoning: false,
			input: ["text"],
			cost: MINIMAX_API_COST,
			contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW,
			maxTokens: MINIMAX_DEFAULT_MAX_TOKENS
		}, {
			id: MINIMAX_DEFAULT_VISION_MODEL_ID,
			name: "MiniMax VL 01",
			reasoning: false,
			input: ["text", "image"],
			cost: MINIMAX_API_COST,
			contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW,
			maxTokens: MINIMAX_DEFAULT_MAX_TOKENS
		}]
	};
}
function buildMinimaxPortalProvider() {
	return {
		baseUrl: MINIMAX_PORTAL_BASE_URL,
		api: "anthropic-messages",
		models: [{
			id: MINIMAX_DEFAULT_MODEL_ID,
			name: "MiniMax M2.1",
			reasoning: false,
			input: ["text"],
			cost: MINIMAX_API_COST,
			contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW,
			maxTokens: MINIMAX_DEFAULT_MAX_TOKENS
		}]
	};
}
function buildMoonshotProvider() {
	return {
		baseUrl: MOONSHOT_BASE_URL,
		api: "openai-completions",
		models: [{
			id: MOONSHOT_DEFAULT_MODEL_ID,
			name: "Kimi K2.5",
			reasoning: false,
			input: ["text"],
			cost: MOONSHOT_DEFAULT_COST,

[1619 more lines in file. Use offset=1110 to continue.]


These are hardcoded provider builders for MiniMax, Moonshot, etc. They have their API formats hardcoded. But the zai provider is user-configured, so it should use the config.

Let me check if there's something specific happening with zai. Let me search for where zai might be getting its API set to openai-completions:

grep -rn "zai.*api\|\"zai\".*openai\|openai.*zai" /usr/lib/node_modules/openclaw/dist/*.js 2>/dev/null | grep -v "zaiApiKey\|zai-api-key\|getZai\|resolveZai" | head -20


/usr/lib/node_modules/openclaw/dist/extensionAPI.js:955:	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
/usr/lib/node_modules/openclaw/dist/extensionAPI.js:23617:	const apiProfile = [...listProfilesForProvider(store, "zai"), ...listProfilesForProvider(store, "z-ai")].find((id) => store.profiles[id]?.type === "api_key");
/usr/lib/node_modules/openclaw/dist/loader-Ds3or8QX.js:3510:	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
/usr/lib/node_modules/openclaw/dist/loader-Ds3or8QX.js:18272:	const apiProfile = [...listProfilesForProvider(store, "zai"), ...listProfilesForProvider(store, "z-ai")].find((id) => store.profiles[id]?.type === "api_key");
/usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js:885:	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
/usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js:23448:	const apiProfile = [...listProfilesForProvider(store, "zai"), ...listProfilesForProvider(store, "z-ai")].find((id) => store.profiles[id]?.type === "api_key");
/usr/lib/node_modules/openclaw/dist/reply-DptDUVRg.js:5602:	if (!(model.provider === "zai" || baseUrl.includes("api.z.ai")) || !isOpenAiCompletionsModel(model)) return model;
/usr/lib/node_modules/openclaw/dist/reply-DptDUVRg.js:33934:	const apiProfile = [...listProfilesForProvider(store, "zai"), ...listProfilesForProvider(store, "z-ai")].find((id) => store.profiles[id]?.type === "api_key");


I see `normalizeModelCompat` is checking if the model is a zai model AND is already an `openai-completions` model. This doesn't change the API format, it just adds compatibility settings.

Let me look at where the model is actually being called and the API format being used. Let me look at the pi-embedded runner more closely to see how the model is being resolved during the actual run:


				} catch (err) {
					if (candidate && candidate === lockedProfileId) throw err;
					nextIndex += 1;
				}
			}
			return false;
		};
		try {
			while (profileIndex < profileCandidates.length) {
				const candidate = profileCandidates[profileIndex];
				if (candidate && candidate !== lockedProfileId && isProfileInCooldown(authStore, candidate)) {
					profileIndex += 1;
					continue;
				}
				await applyApiKeyInfo(profileCandidates[profileIndex]);
				break;
			}
			if (profileIndex >= profileCandidates.length) throwAuthProfileFailover({ allInCooldown: true });
		} catch (err) {
			if (err instanceof FailoverError) throw err;
			if (profileCandidates[profileIndex] === lockedProfileId) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
			if (!await advanceAuthProfile()) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
		}
		const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
		let overflowCompactionAttempts = 0;
		let toolResultTruncationAttempted = false;
		const usageAccumulator = createUsageAccumulator();
		let autoCompactionCount = 0;
		try {
			while (true) {
				attemptedThinking.add(thinkLevel);
				await fs.mkdir(resolvedWorkspace, { recursive: true });
				const prompt = provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
				const attempt = await runEmbeddedAttempt({
					sessionId: params.sessionId,
					sessionKey: params.sessionKey,
					messageChannel: params.messageChannel,
					messageProvider: params.messageProvider,
					agentAccountId: params.agentAccountId,
					messageTo: params.messageTo,
					messageThreadId: params.messageThreadId,
					groupId: params.groupId,
					groupChannel: params.groupChannel,
					groupSpace: params.groupSpace,

[332 more lines in file. Use offset=68020 to continue.]


Let me look at the `runEmbeddedAttempt` function to see how it uses the model and provider:

grep -n "runEmbeddedAttempt\|function runEmbeddedAttempt" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | head -5

grep -n "api.*format\|format.*api\|_api\|\.api" /usr/lib/node_modules/openclaw/dist/pi-embedded-CWm3BvmA.js 2>/dev/null | grep -v "apiKey\|baseUrl\|napi\|apidoc\|apidocs" | head -30


67125:async function runEmbeddedAttempt(params) {
68009:				const attempt = await runEmbeddedAttempt({


881:	return model.api === "openai-completions";
963:			api: model.api ?? entry?.api
1014:				api: providerCfg?.api ?? "openai-responses",
14457:	const api = entry?.api?.trim();
15088:		const apiSuffix = endpoint.api ? ` api: ${endpoint.api}` : "";
24513:		return `api-key ${formatApiKeySnippet$1(profile.key ?? "")}${label ? ` (${label})` : ""}`;
24521:	if (customKey) return `api-key ${formatApiKeySnippet$1(customKey)} (models.json)`;
33906:		return `api-key ${formatApiKeySnippet(profile.key ?? "")}${label ? ` (${label})` : ""}`;
33914:	if (customKey) return `api-key ${formatApiKeySnippet(customKey)} (models.json)`;
36077:		error: "missing_perplexity_api_key",
36082:		error: "missing_xai_api_key",
36087:		error: "missing_brave_api_key",
43936:	const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
44133:	const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
44168:	const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
44197:	const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
44266:	const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
55322:			if (typeof client.apiCall === "function") await client.apiCall("assistant.threads.setStatus", payload);
55374:		const incomingApiAppId = typeof raw.api_app_id === "string" ? raw.api_app_id : "";
55376:		if (params.apiAppId && incomingApiAppId && incomingApiAppId !== params.apiAppId) {
55377:			logVerbose(`slack: drop event with api_app_id=${incomingApiAppId} (expected ${params.apiAppId})`);
55394:		apiAppId: params.apiAppId,
57352:		apiAppId = auth.api_app_id ?? "";
57848:					fn: () => bot.api.sendAnimation(chatId, file, { ...mediaParams })
57855:					fn: () => bot.api.sendPhoto(chatId, file, { ...mediaParams })
57862:					fn: () => bot.api.sendVideo(chatId, file, { ...mediaParams })
57879:							fn: () => bot.api.sendVoice(chatId, file, { ...mediaParams })
57910:						fn: () => bot.api.sendAudio(chatId, file, { ...mediaParams })
57918:					fn: () => bot.api.sendDocument(chatId, file, { ...mediaParams })
58077:			fn: () => bot.api.sendMessage(chatId, htmlText, {


Let me look at line 963 which seems to be in the `buildInlineProviderModels` function. This shows `api: model.api ?? entry?.api`, which means the model's API format is inherited from the provider entry if not set on the model.Let me trace through the full flow. I need to understand how the model is being resolved and where the API format is getting lost.Let me look at where the model is actually called - in the `runEmbeddedAttempt` function:


			}
		}
	}
	return didMutate;
}
async function runEmbeddedAttempt(params) {
	const resolvedWorkspace = resolveUserPath(params.workspaceDir);
	const prevCwd = process.cwd();
	const runAbortController = new AbortController();
	log$2.debug(`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${params.provider} model=${params.modelId} thinking=${params.thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`);
	await fs.mkdir(resolvedWorkspace, { recursive: true });
	const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
	const sandbox = await resolveSandboxContext({
		config: params.config,
		sessionKey: sandboxSessionKey,
		workspaceDir: resolvedWorkspace
	});
	const effectiveWorkspace = sandbox?.enabled ? sandbox.workspaceAccess === "rw" ? resolvedWorkspace : sandbox.workspaceDir : resolvedWorkspace;
	await fs.mkdir(effectiveWorkspace, { recursive: true });
	let restoreSkillEnv;
	process.chdir(effectiveWorkspace);
	try {
		const shouldLoadSkillEntries = !params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
		const skillEntries = shouldLoadSkillEntries ? loadWorkspaceSkillEntries(effectiveWorkspace) : [];
		restoreSkillEnv = params.skillsSnapshot ? applySkillEnvOverridesFromSnapshot({
			snapshot: params.skillsSnapshot,
			config: params.config
		}) : applySkillEnvOverrides({
			skills: skillEntries ?? [],
			config: params.config
		});
		const skillsPrompt = resolveSkillsPromptForRun({
			skillsSnapshot: params.skillsSnapshot,
			entries: shouldLoadSkillEntries ? skillEntries : void 0,
			config: params.config,
			workspaceDir: effectiveWorkspace
		});
		const sessionLabel = params.sessionKey ?? params.sessionId;
		const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles } = await resolveBootstrapContextForRun({
			workspaceDir: effectiveWorkspace,
			config: params.config,
			sessionKey: params.sessionKey,
			sessionId: params.sessionId,
			warn: makeBootstrapWarn({
				sessionLabel,
				warn: (message) => log$2.warn(message)
			})
		});
		const workspaceNotes = hookAdjustedBootstrapFiles.some((file) => file.name === DEFAULT_BOOTSTRAP_FILENAME && !file.missing) ? ["Reminder: commit your changes in this workspace after edits."] : void 0;
		const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
		const modelHasVision = params.model.input?.includes("image") ?? false;
		const tools = sanitizeToolsForGoogle({
			tools: params.disableTools ? [] : createOpenClawCodingTools({
				exec: {
					...params.execOverrides,
					elevated: params.bashElevated
				},
				sandbox,
				messageProvider: params.messageChannel ?? params.messageProvider,
				agentAccountId: params.agentAccountId,
				messageTo: params.messageTo,
				messageThreadId: params.messageThreadId,
				groupId: params.groupId,
				groupChannel: params.groupChannel,
				groupSpace: params.groupSpace,
				spawnedBy: params.spawnedBy,
				senderId: params.senderId,
				senderName: params.senderName,
				senderUsername: params.senderUsername,
				senderE164: params.senderE164,
				senderIsOwner: params.senderIsOwner,
				sessionKey: params.sessionKey ?? params.sessionId,
				agentDir,
				workspaceDir: effectiveWorkspace,
				config: params.config,
				abortSignal: runAbortController.signal,
				modelProvider: params.model.provider,
				modelId: params.modelId,
				modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
				currentChannelId: params.currentChannelId,
				currentThreadTs: params.currentThreadTs,
				replyToMode: params.replyToMode,
				hasRepliedRef: params.hasRepliedRef,
				modelHasVision,
				requireExplicitMessageTarget: params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
				disableMessageTool: params.disableMessageTool
			}),
			provider: params.provider
		});
		logToolSchemasForGoogle({
			tools,
			provider: params.provider
		});
		const machineName = await getMachineDisplayName();
		const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
		let runtimeCapabilities = runtimeChannel ? resolveChannelCapabilities({
			cfg: params.config,
			channel: runtimeChannel,
			accountId: params.agentAccountId
		}) ?? [] : void 0;

[1132 more lines in file. Use offset=67220 to continue.]


I can see the function receives `params.model` which has the model configuration including the `provider` property. The model is passed in from the caller.

Let me look at where `runEmbeddedAttempt` is called and how the model is resolved:


		let overflowCompactionAttempts = 0;
		let toolResultTruncationAttempted = false;
		const usageAccumulator = createUsageAccumulator();
		let autoCompactionCount = 0;
		try {
			while (true) {
				attemptedThinking.add(thinkLevel);
				await fs.mkdir(resolvedWorkspace, { recursive: true });
				const prompt = provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
				const attempt = await runEmbeddedAttempt({
					sessionId: params.sessionId,
					sessionKey: params.sessionKey,
					messageChannel: params.messageChannel,
					messageProvider: params.messageProvider,
					agentAccountId: params.agentAccountId,
					messageTo: params.messageTo,
					messageThreadId: params.messageThreadId,
					groupId: params.groupId,
					groupChannel: params.groupChannel,
					groupSpace: params.groupSpace,
					spawnedBy: params.spawnedBy,
					senderIsOwner: params.senderIsOwner,
					currentChannelId: params.currentChannelId,
					currentThreadTs: params.currentThreadTs,
					replyToMode: params.replyToMode,
					hasRepliedRef: params.hasRepliedRef,
					sessionFile: params.sessionFile,
					workspaceDir: resolvedWorkspace,
					agentDir,
					config: params.config,
					skillsSnapshot: params.skillsSnapshot,
					prompt,
					images: params.images,
					disableTools: params.disableTools,
					provider,
					modelId,
					model,
					authStorage,
					modelRegistry,
					agentId: workspaceResolution.agentId,
					thinkLevel,
					verboseLevel: params.verboseLevel,
					reasoningLevel: params.reasoningLevel,
					toolResultFormat: resolvedToolResultFormat,
					execOverrides: params.execOverrides,
					bashElevated: params.bashElevated,
					timeoutMs: params.timeoutMs,
					runId: params.runId,
					abortSignal: params.abortSignal,
					shouldEmitToolResult: params.shouldEmitToolResult,

[302 more lines in file. Use offset=68050 to continue.]


I see that `model` is passed to `runEmbeddedAttempt`. Let me look at where `model` is resolved earlier in the function:


				} catch (err) {
					if (candidate && candidate === lockedProfileId) throw err;
					nextIndex += 1;
				}
			}
			return false;
		};
		try {
			while (profileIndex < profileCandidates.length) {
				const candidate = profileCandidates[profileIndex];
				if (candidate && candidate !== lockedProfileId && isProfileInCooldown(authStore, candidate)) {
					profileIndex += 1;
					continue;
				}
				await applyApiKeyInfo(profileCandidates[profileIndex]);
				break;
			}
			if (profileIndex >= profileCandidates.length) throwAuthProfileFailover({ allInCooldown: true });
		} catch (err) {
			if (err instanceof FailoverError) throw err;
			if (profileCandidates[profileIndex] === lockedProfileId) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
			if (!await advanceAuthProfile()) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
		}
		const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
		let overflowCompactionAttempts = 0;
		let toolResultTruncationAttempted = false;
		const usageAccumulator = createUsageAccumulator();
		let autoCompactionCount = 0;
		try {
			while (true) {
				attemptedThinking.add(thinkLevel);
				await fs.mkdir(resolvedWorkspace, { recursive: true });
				const prompt = provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
				const attempt = await runEmbeddedAttempt({
					sessionId: params.sessionId,
					sessionKey: params.sessionKey,
					messageChannel: params.messageChannel,
					messageProvider: params.messageProvider,
					agentAccountId: params.agentAccountId,
					messageTo: params.messageTo,
					messageThreadId: params.messageThreadId,
					groupId: params.groupId,
					groupChannel: params.groupChannel,
					groupSpace: params.groupSpace,
					spawnedBy: params.spawnedBy,
					senderIsOwner: params.senderIsOwner,
					currentChannelId: params.currentChannelId,
					currentThreadTs: params.currentThreadTs,
					replyToMode: params.replyToMode,
					hasRepliedRef: params.hasRepliedRef,
					sessionFile: params.sessionFile,
					workspaceDir: resolvedWorkspace,
					agentDir,
					config: params.config,
					skillsSnapshot: params.skillsSnapshot,
					prompt,
					images: params.images,
					disableTools: params.disableTools,
					provider,
					modelId,
					model,
					authStorage,
					modelRegistry,
					agentId: workspaceResolution.agentId,
					thinkLevel,
					verboseLevel: params.verboseLevel,
					reasoningLevel: params.reasoningLevel,
					toolResultFormat: resolvedToolResultFormat,
					execOverrides: params.execOverrides,
					bashElevated: params.bashElevated,
					timeoutMs: params.timeoutMs,
					runId: params.runId,
					abortSignal: params.abortSignal,
					shouldEmitToolResult: params.shouldEmitToolResult,
					shouldEmitToolOutput: params.shouldEmitToolOutput,
					onPartialReply: params.onPartialReply,
					onAssistantMessageStart: params.onAssistantMessageStart,
					onBlockReply: params.onBlockReply,
					onBlockReplyFlush: params.onBlockReplyFlush,
					blockReplyBreak: params.blockReplyBreak,
					blockReplyChunking: params.blockReplyChunking,
					onReasoningStream: params.onReasoningStream,
					onToolResult: params.onToolResult,
					onAgentEvent: params.onAgentEvent,
					extraSystemPrompt: params.extraSystemPrompt,
					streamParams: params.streamParams,
					ownerNumbers: params.ownerNumbers,
					enforceFinalTag: params.enforceFinalTag
				});
				const { aborted, promptError, timedOut, sessionIdUsed, lastAssistant } = attempt;
				mergeUsageIntoAccumulator(usageAccumulator, attempt.attemptUsage ?? normalizeUsage(lastAssistant?.usage));
				autoCompactionCount += Math.max(0, attempt.compactionCount ?? 0);
				const formattedAssistantErrorText = lastAssistant ? formatAssistantErrorText(lastAssistant, {
					cfg: params.config,

[282 more lines in file. Use offset=68070 to continue.]


Let me look earlier in the function to see where `model`, `provider`, and `modelId` are resolved:


			preferredProfile: preferredProfileId
		});
		if (lockedProfileId && !profileOrder.includes(lockedProfileId)) throw new Error(`Auth profile "${lockedProfileId}" is not configured for ${provider}.`);
		const profileCandidates = lockedProfileId ? [lockedProfileId] : profileOrder.length > 0 ? profileOrder : [void 0];
		let profileIndex = 0;
		const initialThinkLevel = params.thinkLevel ?? "off";
		let thinkLevel = initialThinkLevel;
		const attemptedThinking = /* @__PURE__ */ new Set();
		let apiKeyInfo = null;
		let lastProfileId;
		const resolveAuthProfileFailoverReason = (params) => {
			if (params.allInCooldown) return "rate_limit";
			return classifyFailoverReason(params.message) ?? "auth";
		};
		const throwAuthProfileFailover = (params) => {
			const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
			const message = params.message?.trim() || (params.error ? describeUnknownError(params.error).trim() : "") || fallbackMessage;
			const reason = resolveAuthProfileFailoverReason({
				allInCooldown: params.allInCooldown,
				message
			});
			if (fallbackConfigured) throw new FailoverError(message, {
				reason,
				provider,
				model: modelId,
				status: resolveFailoverStatus(reason),
				cause: params.error
			});
			if (params.error instanceof Error) throw params.error;
			throw new Error(message);
		};
		const resolveApiKeyForCandidate = async (candidate) => {
			return getApiKeyForModel({
				model,
				cfg: params.config,
				profileId: candidate,
				store: authStore,
				agentDir
			});
		};
		const applyApiKeyInfo = async (candidate) => {
			apiKeyInfo = await resolveApiKeyForCandidate(candidate);
			const resolvedProfileId = apiKeyInfo.profileId ?? candidate;
			if (!apiKeyInfo.apiKey) {
				if (apiKeyInfo.mode !== "aws-sdk") throw new Error(`No API key resolved for provider "${model.provider}" (auth mode: ${apiKeyInfo.mode}).`);
				lastProfileId = resolvedProfileId;
				return;
			}
			if (model.provider === "github-copilot") {
				const { resolveCopilotApiToken } = await import("./github-copilot-token-C9IJh2Pn.js").then((n) => n.n);
				const copilotToken = await resolveCopilotApiToken({ githubToken: apiKeyInfo.apiKey });
				authStorage.setRuntimeApiKey(model.provider, copilotToken.token);
			} else authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
			lastProfileId = apiKeyInfo.profileId;
		};
		const advanceAuthProfile = async () => {
			if (lockedProfileId) return false;
			let nextIndex = profileIndex + 1;
			while (nextIndex < profileCandidates.length) {
				const candidate = profileCandidates[nextIndex];
				if (candidate && isProfileInCooldown(authStore, candidate)) {
					nextIndex += 1;
					continue;
				}
				try {
					await applyApiKeyInfo(candidate);
					profileIndex = nextIndex;
					thinkLevel = initialThinkLevel;
					attemptedThinking.clear();
					return true;
				} catch (err) {
					if (candidate && candidate === lockedProfileId) throw err;
					nextIndex += 1;
				}
			}
			return false;
		};
		try {
			while (profileIndex < profileCandidates.length) {
				const candidate = profileCandidates[profileIndex];
				if (candidate && candidate !== lockedProfileId && isProfileInCooldown(authStore, candidate)) {
					profileIndex += 1;
					continue;
				}
				await applyApiKeyInfo(profileCandidates[profileIndex]);
				break;
			}
			if (profileIndex >= profileCandidates.length) throwAuthProfileFailover({ allInCooldown: true });
		} catch (err) {
			if (err instanceof FailoverError) throw err;
			if (profileCandidates[profileIndex] === lockedProfileId) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
			if (!await advanceAuthProfile()) throwAuthProfileFailover({
				allInCooldown: false,
				error: err
			});
		}
		const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;

[352 more lines in file. Use offset=68000 to continue.]


I see `provider`, `modelId`, and `model` are used but I need to find where they're defined. Let me look earlier in the function:


const mergeUsageIntoAccumulator = (target, usage) => {
	if (!hasUsageValues(usage)) return;
	target.input += usage.input ?? 0;
	target.output += usage.output ?? 0;
	target.cacheRead += usage.cacheRead ?? 0;
	target.cacheWrite += usage.cacheWrite ?? 0;
	target.total += usage.total ?? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
};
const toNormalizedUsage = (usage) => {
	if (!(usage.input > 0 || usage.output > 0 || usage.cacheRead > 0 || usage.cacheWrite > 0 || usage.total > 0)) return;
	const derivedTotal = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
	return {
		input: usage.input || void 0,
		output: usage.output || void 0,
		cacheRead: usage.cacheRead || void 0,
		cacheWrite: usage.cacheWrite || void 0,
		total: usage.total || derivedTotal || void 0
	};
};
async function runEmbeddedPiAgent(params) {
	const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
	const globalLane = resolveGlobalLane(params.lane);
	const enqueueGlobal = params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
	const enqueueSession = params.enqueue ?? ((task, opts) => enqueueCommandInLane(sessionLane, task, opts));
	const channelHint = params.messageChannel ?? params.messageProvider;
	const resolvedToolResultFormat = params.toolResultFormat ?? (channelHint ? isMarkdownCapableMessageChannel(channelHint) ? "markdown" : "plain" : "markdown");
	const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
	return enqueueSession(() => enqueueGlobal(async () => {
		const started = Date.now();
		const workspaceResolution = resolveRunWorkspaceDir({
			workspaceDir: params.workspaceDir,
			sessionKey: params.sessionKey,
			agentId: params.agentId,
			config: params.config
		});
		const resolvedWorkspace = workspaceResolution.workspaceDir;
		const redactedSessionId = redactRunIdentifier(params.sessionId);
		const redactedSessionKey = redactRunIdentifier(params.sessionKey);
		const redactedWorkspace = redactRunIdentifier(resolvedWorkspace);
		if (workspaceResolution.usedFallback) log$2.warn(`[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`);
		const prevCwd = process.cwd();
		const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
		const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
		const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
		const fallbackConfigured = (params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
		await ensureOpenClawModelsJson(params.config, agentDir);
		const { model, error, authStorage, modelRegistry } = resolveModel$4(provider, modelId, agentDir, params.config);
		if (!model) throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
		const ctxInfo = resolveContextWindowInfo({
			cfg: params.config,
			provider,
			modelId,
			modelContextWindow: model.contextWindow,
			defaultTokens: DEFAULT_CONTEXT_TOKENS
		});
		const ctxGuard = evaluateContextWindowGuard({
			info: ctxInfo,
			warnBelowTokens: CONTEXT_WINDOW_WARN_BELOW_TOKENS,
			hardMinTokens: CONTEXT_WINDOW_HARD_MIN_TOKENS
		});
		if (ctxGuard.shouldWarn) log$2.warn(`low context window: ${provider}/${modelId} ctx=${ctxGuard.tokens} (warn<${CONTEXT_WINDOW_WARN_BELOW_TOKENS}) source=${ctxGuard.source}`);
		if (ctxGuard.shouldBlock) {
			log$2.error(`blocked model (context window too small): ${provider}/${modelId} ctx=${ctxGuard.tokens} (min=${CONTEXT_WINDOW_HARD_MIN_TOKENS}) source=${ctxGuard.source}`);
			throw new FailoverError(`Model context window too small (${ctxGuard.tokens} tokens). Minimum is ${CONTEXT_WINDOW_HARD_MIN_TOKENS}.`, {
				reason: "unknown",
				provider,
				model: modelId
			});
		}
		const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
		const preferredProfileId = params.authProfileId?.trim();
		let lockedProfileId = params.authProfileIdSource === "user" ? preferredProfileId : void 0;
		if (lockedProfileId) {
			const lockedProfile = authStore.profiles[lockedProfileId];
			if (!lockedProfile || normalizeProviderId(lockedProfile.provider) !== normalizeProviderId(provider)) lockedProfileId = void 0;
		}
		const profileOrder = resolveAuthProfileOrder({
			cfg: params.config,
			store: authStore,
			provider,
			preferredProfile: preferredProfileId
		});
		if (lockedProfileId && !profileOrder.includes(lockedProfileId)) throw new Error(`Auth profile "${lockedProfileId}" is not configured for ${provider}.`);
		const profileCandidates = lockedProfileId ? [lockedProfileId] : profileOrder.length > 0 ? profileOrder : [void 0];
		let profileIndex = 0;
		const initialThinkLevel = params.thinkLevel ?? "off";
		let thinkLevel = initialThinkLevel;
		const attemptedThinking = /* @__PURE__ */ new Set();
		let apiKeyInfo = null;
		let lastProfileId;
		const resolveAuthProfileFailoverReason = (params) => {
			if (params.allInCooldown) return "rate_limit";
			return classifyFailoverReason(params.message) ?? "auth";
		};
		const throwAuthProfileFailover = (params) => {
			const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
			const message = params.message?.trim() || (params.error ? describeUnknownError(params.error).trim() : "") || fallbackMessage;
			const reason = resolveAuthProfileFailoverReason({
				allInCooldown: params.allInCooldown,
				message

[432 more lines in file. Use offset=67920 to continue.]


Found it! Look at line 67860-67862:

```javascript
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
...
const { model, error, authStorage, modelRegistry } = resolveModel$4(provider, modelId, agentDir, params.config);
```

So the `provider` comes from `params.provider`, and `modelId` comes from `params.model`. These are passed to `resolveModel$4` which we saw earlier.

Now, the question is: where does `params.provider` and `params.model` come from when calling `runEmbeddedPiAgent`?Let me trace back to where `runEmbeddedPiAgent` is called from. This is likely in the `runWithModelFallback` function we saw earlier in extensionAPI.js:


				text = stripped.text;
			}
			if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) return { skip: true };
			if (!text) return { skip: true };
			const sanitized = sanitizeUserFacingText(text);
			if (!sanitized.trim()) return { skip: true };
			return {
				text: sanitized,
				skip: false
			};
		};
		const handlePartialForTyping = async (payload) => {
			const { text, skip } = normalizeStreamingText(payload);
			if (skip || !text) return;
			await params.typingSignals.signalTextDelta(text);
			return text;
		};
		const blockReplyPipeline = params.blockReplyPipeline;
		const onToolResult = params.opts?.onToolResult;
		const fallbackResult = await runWithModelFallback({
			cfg: params.followupRun.run.config,
			provider: params.followupRun.run.provider,
			model: params.followupRun.run.model,
			agentDir: params.followupRun.run.agentDir,
			fallbacksOverride: resolveAgentModelFallbacksOverride(params.followupRun.run.config, resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey)),
			run: (provider, model) => {
				params.opts?.onModelSelected?.({
					provider,
					model,
					thinkLevel: params.followupRun.run.thinkLevel
				});
				if (isCliProvider(provider, params.followupRun.run.config)) {
					const startedAt = Date.now();
					emitAgentEvent({
						runId,
						stream: "lifecycle",
						data: {
							phase: "start",
							startedAt
						}
					});
					const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
					return (async () => {
						let lifecycleTerminalEmitted = false;
						try {
							const result = await runCliAgent({
								sessionId: params.followupRun.run.sessionId,
								sessionKey: params.sessionKey,
								agentId: params.followupRun.run.agentId,
								sessionFile: params.followupRun.run.sessionFile,

[30358 more lines in file. Use offset=38130 to continue.]