Skip to content

Commit 7394792

Browse files
committed
fix(models): hide openrouter nested provider duplicates
1 parent 0a387bf commit 7394792

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
6363
- Channels/iMessage: wire `action: "reply"` attachments through `imsg send-rich --file` when the installed imsg build advertises that capability (probed once via `imsg send-rich --help` and cached on the private-API status). Reply now hydrates `media`/`mediaUrl`/`fileUrl`/`mediaUrls[0]`/`filePath`/`path`/base64 `buffer`+`filename` through the shared outbound resolver, stages buffers via the existing `withTempFile` helper, rejects `http(s)://` URL attachments with a targeted error pointing callers at `send`'s full attachment-resolver pipeline, and falls back to the explicit `imsg#114 not landed yet` error on older imsg builds. Depends on the upstream `openclaw/imsg#114` capability landing in an installable release; until then the new path stays gated and users see the same explicit fallback `#79822` introduced. (#79864) Thanks @omarshahine.
6464
- Telegram: preserve the first-preview debounce while appending true partial-stream deltas, so edited draft previews no longer duplicate earlier text when providers emit incremental output. (#80045) Thanks @TurboTheTurtle.
6565
- Agents/Anthropic: report 1M session context for Claude Opus/Sonnet 4 models even when local model config still advertises 200k, matching model discovery and preventing premature status/UI overflow. Fixes #66766.
66+
- Models/OpenRouter: hide missing-auth direct provider rows in `/model status` when they are only duplicated by a nested OpenRouter model id such as `openrouter/google/...`, while preserving explicitly configured direct providers. Fixes #62317.
6667
- Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with `minLength`. Fixes #38817.
6768
- DeepSeek: backfill V4 `reasoning_content` replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608.
6869

src/auto-reply/reply/directive-handling.model.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,75 @@ describe("/model chat UX", () => {
517517
expect(reply?.text).not.toContain("missing (missing)");
518518
});
519519

520+
it("hides missing-auth direct provider rows covered by OpenRouter nested model ids", async () => {
521+
const reply = await resolveModelInfoReply({
522+
directives: parseInlineDirectives("/model status"),
523+
provider: "openrouter",
524+
model: "google/gemini-3-flash-preview",
525+
defaultProvider: "openrouter",
526+
defaultModel: "google/gemini-3-flash-preview",
527+
cfg: {
528+
commands: { text: true },
529+
models: {
530+
providers: {
531+
openrouter: {
532+
models: [{ id: "google/gemini-3-flash-preview", name: "Gemini via OpenRouter" }],
533+
},
534+
},
535+
},
536+
} as OpenClawConfig,
537+
allowedModelCatalog: [
538+
{ provider: "google", id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
539+
{
540+
provider: "openrouter",
541+
id: "google/gemini-3-flash-preview",
542+
name: "Gemini via OpenRouter",
543+
},
544+
],
545+
});
546+
547+
expect(reply?.text).toContain("[openrouter]");
548+
expect(reply?.text).toContain("openrouter/google/gemini-3-flash-preview");
549+
expect(reply?.text).not.toContain("\n[google]");
550+
expect(reply?.text).not.toContain("\n • google/gemini-3-flash-preview");
551+
});
552+
553+
it("keeps explicitly configured direct provider rows next to OpenRouter nested ids", async () => {
554+
const reply = await resolveModelInfoReply({
555+
directives: parseInlineDirectives("/model status"),
556+
provider: "openrouter",
557+
model: "google/gemini-3-flash-preview",
558+
defaultProvider: "openrouter",
559+
defaultModel: "google/gemini-3-flash-preview",
560+
cfg: {
561+
commands: { text: true },
562+
models: {
563+
providers: {
564+
google: {
565+
models: [{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash" }],
566+
},
567+
openrouter: {
568+
models: [{ id: "google/gemini-3-flash-preview", name: "Gemini via OpenRouter" }],
569+
},
570+
},
571+
},
572+
} as OpenClawConfig,
573+
allowedModelCatalog: [
574+
{ provider: "google", id: "gemini-3-flash-preview", name: "Gemini 3 Flash" },
575+
{
576+
provider: "openrouter",
577+
id: "google/gemini-3-flash-preview",
578+
name: "Gemini via OpenRouter",
579+
},
580+
],
581+
});
582+
583+
expect(reply?.text).toContain("[google]");
584+
expect(reply?.text).toContain("google/gemini-3-flash-preview");
585+
expect(reply?.text).toContain("[openrouter]");
586+
expect(reply?.text).toContain("openrouter/google/gemini-3-flash-preview");
587+
});
588+
520589
it("reports Codex runtime auth for OpenAI status rows", async () => {
521590
setAuthProfiles({
522591
"openai-codex:[email protected]": {

src/auto-reply/reply/directive-handling.model.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles.js";
22
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
33
import {
44
type ModelAliasIndex,
5+
buildConfiguredModelCatalog,
56
modelKey,
67
normalizeProviderId,
78
resolveConfiguredModelRef,
@@ -263,6 +264,46 @@ function buildModelPickerCatalog(params: {
263264
return out;
264265
}
265266

267+
function filterMissingAuthNestedProviderDuplicates(params: {
268+
cfg: OpenClawConfig;
269+
entries: ModelPickerCatalogEntry[];
270+
authByProvider: Map<string, string>;
271+
}): ModelPickerCatalogEntry[] {
272+
const configuredKeys = new Set(
273+
buildConfiguredModelCatalog({ cfg: params.cfg }).map((entry) =>
274+
modelKey(entry.provider, entry.id),
275+
),
276+
);
277+
const wrapperKeys = new Set<string>();
278+
for (const entry of params.entries) {
279+
const id = normalizeOptionalString(entry.id) ?? "";
280+
const slash = id.indexOf("/");
281+
if (slash <= 0) {
282+
continue;
283+
}
284+
const nestedProvider = normalizeProviderId(id.slice(0, slash));
285+
const nestedModel = normalizeOptionalString(id.slice(slash + 1)) ?? "";
286+
const wrapperProvider = normalizeProviderId(entry.provider);
287+
if (!nestedProvider || !nestedModel || nestedProvider === wrapperProvider) {
288+
continue;
289+
}
290+
wrapperKeys.add(modelKey(nestedProvider, nestedModel));
291+
}
292+
if (wrapperKeys.size === 0) {
293+
return params.entries;
294+
}
295+
296+
return params.entries.filter((entry) => {
297+
const provider = normalizeProviderId(entry.provider);
298+
const id = normalizeOptionalString(entry.id) ?? "";
299+
const key = modelKey(provider, id);
300+
if (configuredKeys.has(key)) {
301+
return true;
302+
}
303+
return params.authByProvider.get(provider) !== "missing" || !wrapperKeys.has(key);
304+
});
305+
}
306+
266307
export async function maybeHandleModelDirectiveInfo(params: {
267308
directives: InlineDirectives;
268309
cfg: OpenClawConfig;
@@ -409,7 +450,12 @@ export async function maybeHandleModelDirectiveInfo(params: {
409450
}
410451

411452
const byProvider = new Map<string, ModelPickerCatalogEntry[]>();
412-
for (const entry of pickerCatalog) {
453+
const statusCatalog = filterMissingAuthNestedProviderDuplicates({
454+
cfg: params.cfg,
455+
entries: pickerCatalog,
456+
authByProvider,
457+
});
458+
for (const entry of statusCatalog) {
413459
const provider = normalizeProviderId(entry.provider);
414460
const models = byProvider.get(provider);
415461
if (models) {

0 commit comments

Comments
 (0)