diff --git a/packages/agent-chat/src/gateway.ts b/packages/agent-chat/src/gateway.ts index a28a400..affb594 100644 --- a/packages/agent-chat/src/gateway.ts +++ b/packages/agent-chat/src/gateway.ts @@ -484,6 +484,15 @@ export type GatewayClient = { priceable?: boolean; }>; }>; + /** Owner-authorized official models (Host table) + this Host's key geo. */ + getMyAgentOfficialModels: (agentId: string) => Promise<{ + agent_id: string; + model_ids: string[]; + host_inference_ready: boolean; + official_key_geo?: string; + official_region?: string; + official_default_model_id?: string | null; + }>; /** Replace Owner-authorized official models. Empty = all hops BYO. */ updateMyAgentOfficialModels: ( agentId: string, @@ -492,6 +501,9 @@ export type GatewayClient = { agent_id: string; model_ids: string[]; host_inference_ready: boolean; + official_key_geo?: string; + official_region?: string; + official_default_model_id?: string | null; }>; /** Rotate ACN API key; plaintext returned once — do not log. */ rotateMyAgentKey: (agentId: string) => Promise<{ @@ -756,11 +768,23 @@ export function createGatewayClient( priceable?: boolean; }>; }>(`/api/chat/agents/${encodeURIComponent(agentId)}/model-status`), + getMyAgentOfficialModels: (agentId) => + request<{ + agent_id: string; + model_ids: string[]; + host_inference_ready: boolean; + official_key_geo?: string; + official_region?: string; + official_default_model_id?: string | null; + }>(`/api/chat/my-agents/${encodeURIComponent(agentId)}/official-models`), updateMyAgentOfficialModels: (agentId, modelIds) => request<{ agent_id: string; model_ids: string[]; host_inference_ready: boolean; + official_key_geo?: string; + official_region?: string; + official_default_model_id?: string | null; }>(`/api/chat/my-agents/${encodeURIComponent(agentId)}/official-models`, { method: "PUT", body: JSON.stringify({ model_ids: modelIds }), diff --git a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx index 10cfa71..a218e10 100644 --- a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx +++ b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx @@ -12,6 +12,7 @@ import { import { createPortal } from "react-dom"; import { ChatGatewayError, + officialCatalogRates, syncCatalogRates, type ChatAgentSearchHit, type GatewayClient, @@ -20,6 +21,7 @@ import { } from "../gateway"; import { copyText } from "./connectPrompt"; import type { RanchMessages } from "./i18n"; +import { officialShelfAllows } from "./officialV0"; import { btnGhost, btnPrimary, colors, inputStyle } from "./styles"; /** Align with ACN / Gateway display-name rules (letter required). */ @@ -101,12 +103,18 @@ function providerIdForModel(modelId: string, supported: string[]): string { return OPENROUTER_BYO; } -/** Settings Provider follows the live runtime, not a leftover OpenRouter listing. */ +/** Settings Provider follows official listing when authorized; else live runtime. */ function providerIdFromRuntime( runtime: string, listed: string, supported: string[], + official: string[] = [], + hostReady = false, ): string { + const ls = listed.trim(); + if (hostReady && ls && modelIsOfficial(ls, official)) { + return OFFICIAL_OPENROUTER; + } const rt = (runtime || "").trim(); if (rt) { const vendor = modelVendorId(rt); @@ -114,7 +122,7 @@ function providerIdFromRuntime( if (isKnownByoVendor(vendor, supported)) return vendor; return OPENROUTER_BYO; } - return providerIdForModel(listed, supported); + return providerIdForModel(ls, supported); } function runtimeIsOpenRouter(runtime: string, supported: string[]): boolean { @@ -128,11 +136,13 @@ function listingIsStaleOpenRouter( listed: string, runtime: string, supported: string[] = [], + official: string[] = [], ): boolean { const ls = listed.trim(); const rt = runtime.trim(); if (!ls || !rt) return false; if (sameModelId(ls, rt)) return false; + if (modelIsOfficial(ls, official)) return false; const rtVendor = modelVendorId(rt); const lsVendor = modelVendorId(ls); if (!rtVendor || !lsVendor) return false; @@ -161,12 +171,16 @@ function nextOfficialModels( function resolvePricingModelId( detail: MyAgentSummary, supported: string[] = [], + official: string[] = [], ): string { - // Heartbeat default wins. Stale listing must not impersonate the machine. + const listed = (detail.token_pricing?.model_id || "").trim(); const runtime = (detail.runtime_model_id || "").trim(); + if (listed && modelIsOfficial(listed, official)) return listed; + // Heartbeat default wins for BYO. Stale Store listing must not impersonate the machine. if (runtime) return runtime; - const listed = (detail.token_pricing?.model_id || "").trim(); - if (listed && !listingIsStaleOpenRouter(listed, runtime, supported)) return listed; + if (listed && !listingIsStaleOpenRouter(listed, runtime, supported, official)) { + return listed; + } const preferred = (detail.preferred_model_id || "").trim(); if (preferred) return preferred; return FALLBACK_MODEL_ID; @@ -226,6 +240,7 @@ function catalogSourceHref( } const OPENROUTER_BYO = "openrouter"; +const OFFICIAL_OPENROUTER = "official_openrouter"; const OTHER_VENDOR = "__other__"; function storeOpenRouterUrl(base?: string): string { @@ -262,7 +277,7 @@ function modelsForVendor(ids: string[], vendor: string): string[] { function modelsForProvider(ids: string[], provider: string): string[] { if (!provider) return []; - if (provider === OPENROUTER_BYO) return []; + if (provider === OPENROUTER_BYO || provider === OFFICIAL_OPENROUTER) return []; if (provider === OTHER_VENDOR) return modelsForVendor(ids, ""); return modelsForVendor(ids, provider); } @@ -872,25 +887,34 @@ export function AgentOwnerSettings({ const [savingProfile, setSavingProfile] = useState(false); const [profileMsg, setProfileMsg] = useState(null); const [profileError, setProfileError] = useState(null); - const [modelIdDraft, setModelIdDraft] = useState(() => resolvePricingModelId(detail)); + const [modelIdDraft, setModelIdDraft] = useState(() => + resolvePricingModelId(detail, [], detail.official_models ?? []), + ); const [supportedModels, setSupportedModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(true); const [hostReady, setHostReady] = useState(Boolean(detail.host_inference_ready)); const [officialSaved, setOfficialSaved] = useState( () => detail.official_models ?? [], ); + const [officialKeyGeo, setOfficialKeyGeo] = useState(""); + const [officialDefaultModelId, setOfficialDefaultModelId] = useState(""); const [savingOfficial, setSavingOfficial] = useState(false); const [officialMsg, setOfficialMsg] = useState(null); const [officialError, setOfficialError] = useState(null); const [officialCatalog, setOfficialCatalog] = useState< Array<{ id: string } & CatalogPair> >([]); + const [openRouterByoCatalog, setOpenRouterByoCatalog] = useState< + Array<{ id: string } & CatalogPair> + >([]); const [officialCatalogLoading, setOfficialCatalogLoading] = useState(false); const [settingsProvider, setSettingsProvider] = useState(() => providerIdFromRuntime( detail.runtime_model_id || "", - resolvePricingModelId(detail), + resolvePricingModelId(detail, [], detail.official_models ?? []), [], + detail.official_models ?? [], + Boolean(detail.host_inference_ready), ), ); const [markupDraft, setMarkupDraft] = useState(() => { @@ -956,7 +980,8 @@ export function AgentOwnerSettings({ }, [detail.agent_id, detail.name, detail.description, detail.tags?.join("\u0001")]); useEffect(() => { - setModelIdDraft(resolvePricingModelId(detail, supportedModels)); + const official = detail.official_models ?? []; + setModelIdDraft(resolvePricingModelId(detail, supportedModels, official)); const mu = detail.token_pricing?.markup_percent; setMarkupDraft( typeof mu === "number" && Number.isFinite(mu) && mu >= 0 @@ -966,15 +991,16 @@ export function AgentOwnerSettings({ setPricingMsg(null); setPricingError(null); setHostReady(Boolean(detail.host_inference_ready)); - const official = detail.official_models ?? []; setOfficialSaved(official); setOfficialMsg(null); setOfficialError(null); setSettingsProvider( providerIdFromRuntime( detail.runtime_model_id || "", - resolvePricingModelId(detail, supportedModels), + resolvePricingModelId(detail, supportedModels, official), supportedModels, + official, + Boolean(detail.host_inference_ready), ), ); }, [ @@ -990,9 +1016,11 @@ export function AgentOwnerSettings({ useEffect(() => { let cancelled = false; setModelsLoading(true); - client - .getAgentModelStatus(detail.agent_id) - .then((status) => { + void Promise.all([ + client.getAgentModelStatus(detail.agent_id), + client.getMyAgentOfficialModels(detail.agent_id).catch(() => null), + ]) + .then(([status, officialRow]) => { if (cancelled) return; const reported = uniqModelIds(status.self_reported_models); const ids = reported.length @@ -1004,15 +1032,31 @@ export function AgentOwnerSettings({ if (typeof status.host_inference_ready === "boolean") { setHostReady(status.host_inference_ready); } - if (Array.isArray(status.official_models)) { - setOfficialSaved(status.official_models); + const official = Array.isArray(status.official_models) + ? status.official_models + : officialRow?.model_ids ?? detail.official_models ?? []; + setOfficialSaved(official); + if (officialRow?.official_key_geo) { + setOfficialKeyGeo(officialRow.official_key_geo); } - setModelIdDraft(resolvePricingModelId(detail, ids)); + if (officialRow?.official_default_model_id) { + setOfficialDefaultModelId(officialRow.official_default_model_id); + } + if (typeof officialRow?.host_inference_ready === "boolean") { + setHostReady(officialRow.host_inference_ready); + } + setModelIdDraft(resolvePricingModelId(detail, ids, official)); setSettingsProvider( providerIdFromRuntime( status.runtime_model_id || detail.runtime_model_id || "", - resolvePricingModelId(detail, ids), + resolvePricingModelId(detail, ids, official), ids, + official, + Boolean( + officialRow?.host_inference_ready ?? + status.host_inference_ready ?? + detail.host_inference_ready, + ), ), ); }) @@ -1033,28 +1077,6 @@ export function AgentOwnerSettings({ }; }, [client, detail.agent_id, detail.token_pricing?.model_id, detail.runtime_model_id]); - useEffect(() => { - if (officialSaved.length === 0) return; - let cancelled = false; - void client - .updateMyAgentOfficialModels(detail.agent_id, []) - .then((row) => { - if (cancelled) return; - setOfficialSaved(row.model_ids); - onUpdated?.({ - ...detail, - official_models: row.model_ids, - host_inference_ready: row.host_inference_ready, - }); - }) - .catch(() => { - // Host may not expose official-models yet; send path still forces byo. - }); - return () => { - cancelled = true; - }; - }, [client, detail.agent_id, officialSaved.join("\u0001")]); - const supportedKey = supportedModels.join("\u0001"); useEffect(() => { if (supportedModels.length === 0) { @@ -1097,7 +1119,8 @@ export function AgentOwnerSettings({ setOfficialCatalogLoading(true); void (async () => { const page = 500; - const acc: Array<{ id: string } & CatalogPair> = []; + const byoAcc: Array<{ id: string } & CatalogPair> = []; + const officialAcc: Array<{ id: string } & CatalogPair> = []; let offset = 0; let total = Number.POSITIVE_INFINITY; try { @@ -1112,25 +1135,44 @@ export function AgentOwnerSettings({ for (const row of data.items) { const src = (row.source || "openrouter").toLowerCase(); if (src && src !== "openrouter") continue; - const quote = syncCatalogRates(row); - if (!quote) continue; const id = (row.model_id || "").trim(); if (!id) continue; - if (acc.some((item) => sameModelId(item.id, id))) continue; - acc.push({ - id, - in: quote.input, - out: quote.output, - source: "openrouter", - }); + const byoQuote = syncCatalogRates(row); + if (byoQuote && !byoAcc.some((item) => sameModelId(item.id, id))) { + byoAcc.push({ + id, + in: byoQuote.input, + out: byoQuote.output, + source: "openrouter", + }); + } + const officialQuote = officialCatalogRates(row); + if ( + officialQuote && + hostReady && + officialKeyGeo && + officialShelfAllows(id, officialKeyGeo) && + !officialAcc.some((item) => sameModelId(item.id, id)) + ) { + officialAcc.push({ + id, + in: officialQuote.input, + out: officialQuote.output, + source: "openrouter", + }); + } } if (!data.items.length) break; offset += data.items.length; } if (cancelled) return; - setOfficialCatalog(acc); + setOpenRouterByoCatalog(byoAcc); + setOfficialCatalog(officialAcc); } catch { - if (!cancelled) setOfficialCatalog([]); + if (!cancelled) { + setOpenRouterByoCatalog([]); + setOfficialCatalog([]); + } } finally { if (!cancelled) setOfficialCatalogLoading(false); } @@ -1138,7 +1180,7 @@ export function AgentOwnerSettings({ return () => { cancelled = true; }; - }, [client]); + }, [client, hostReady, officialKeyGeo]); useEffect(() => { setDeliveryDraft(deliveryFromDetail(detail.delivery)); @@ -1198,6 +1240,7 @@ export function AgentOwnerSettings({ oldModelId, runtimeId, supportedModels, + officialSaved, ); const listingPublished = !listingStale && @@ -1205,11 +1248,15 @@ export function AgentOwnerSettings({ typeof oldMarkup === "number" && Number.isFinite(oldMarkup); const officialIds = officialCatalog.map((row) => row.id); + const byoOpenRouterIds = openRouterByoCatalog.map((row) => row.id); const byoVendors = vendorsFromModels(supportedModels).filter( (id) => id.toLowerCase() !== OPENROUTER_BYO, ); const hasBareModels = supportedModels.some((id) => !modelVendorId(id)); const providerOptions: Array<{ id: string; label: string }> = [ + ...(hostReady + ? [{ id: OFFICIAL_OPENROUTER, label: t.myAgentsProviderOfficialOpenRouter }] + : []), ...byoVendors.map((id) => ({ id, label: id })), ...(hasBareModels ? [{ id: OTHER_VENDOR, label: t.myAgentsProviderOther }] : []), { id: OPENROUTER_BYO, label: t.myAgentsProviderOpenRouter }, @@ -1217,6 +1264,7 @@ export function AgentOwnerSettings({ if ( settingsProvider && settingsProvider !== OPENROUTER_BYO && + settingsProvider !== OFFICIAL_OPENROUTER && !providerOptions.some((p) => p.id === settingsProvider) ) { providerOptions.unshift({ @@ -1234,14 +1282,20 @@ export function AgentOwnerSettings({ : providerOptions.find((p) => p.id !== OPENROUTER_BYO)?.id || providerOptions[0]?.id || ""; - const officialSelected = activeProvider === OPENROUTER_BYO; + const officialSelected = activeProvider === OFFICIAL_OPENROUTER; + const openRouterByoSelected = activeProvider === OPENROUTER_BYO; const vendorModels = officialSelected ? officialIds - : modelsForProvider(supportedModels, activeProvider); + : openRouterByoSelected + ? byoOpenRouterIds + : modelsForProvider(supportedModels, activeProvider); const displayedModelId = pickListedId(vendorModels, modelIdTrim); const officialRow = officialCatalog.find((row) => sameModelId(row.id, displayedModelId), ); + const byoOpenRouterRow = openRouterByoCatalog.find((row) => + sameModelId(row.id, displayedModelId), + ); const selectedCatalog = officialSelected ? officialRow ? { @@ -1250,29 +1304,42 @@ export function AgentOwnerSettings({ source: officialRow.source || "openrouter", } : null - : catalogById[displayedModelId] ?? - Object.entries(catalogById).find(([id]) => sameModelId(id, displayedModelId))?.[1] ?? - null; + : openRouterByoSelected + ? byoOpenRouterRow + ? { + in: byoOpenRouterRow.in, + out: byoOpenRouterRow.out, + source: byoOpenRouterRow.source || "openrouter", + } + : null + : catalogById[displayedModelId] ?? + Object.entries(catalogById).find(([id]) => sameModelId(id, displayedModelId))?.[1] ?? + null; const catalogIn = selectedCatalog?.in ?? null; const catalogOut = selectedCatalog?.out ?? null; const catalogSource = (() => { const fromRow = (selectedCatalog?.source || "").trim().toLowerCase(); if (fromRow) return fromRow; - if (officialSelected) return "openrouter"; + if (officialSelected || openRouterByoSelected) return "openrouter"; if (modelVendorId(displayedModelId).toLowerCase() === "tencenttokenplan") { return "host_pack"; } return ""; })(); const catalogSourceUrl = catalogSourceHref(catalogSource, displayedModelId); - const catalogLoading = officialSelected - ? officialCatalogLoading - : supportedModels.length > 0 && catalogReadyKey !== supportedKey; + const catalogLoading = + officialSelected || openRouterByoSelected + ? officialCatalogLoading + : supportedModels.length > 0 && catalogReadyKey !== supportedKey; const catalogError = !catalogLoading && displayedModelId.length > 0 && selectedCatalog == null && - (officialSelected ? officialCatalog.length > 0 : supportedModels.length > 0) + (officialSelected + ? officialCatalog.length > 0 + : openRouterByoSelected + ? openRouterByoCatalog.length > 0 + : supportedModels.length > 0) ? t.myAgentsPricingCatalogMissing : null; const inputParsed = @@ -1291,17 +1358,18 @@ export function AgentOwnerSettings({ !sameModelId(displayedModelId, oldModelId) || markupParsed !== oldMarkup); const runtimeMismatch = Boolean( - runtimeId && !sameModelId(runtimeId, displayedModelId), + runtimeId && !sameModelId(runtimeId, displayedModelId) && !officialSelected, ); const modelOnList = vendorModels.length > 0 && vendorModels.some((id) => sameModelId(id, displayedModelId)); - const modelsBusy = officialSelected ? officialCatalogLoading : modelsLoading; + const modelsBusy = + officialSelected || openRouterByoSelected ? officialCatalogLoading : modelsLoading; const openRouterOnRuntime = runtimeIsOpenRouter( detail.runtime_model_id || "", supportedModels, ); - const openRouterBlocked = officialSelected && !openRouterOnRuntime; + const openRouterBlocked = openRouterByoSelected && !openRouterOnRuntime; const canSavePricing = pricingDirty && previewReady && @@ -1326,8 +1394,12 @@ export function AgentOwnerSettings({ displayedModelId, officialSelected, ); - const officialDirty = false; - const canSaveOfficial = false; + const officialDirty = + hostReady && + displayedModelId.length > 0 && + !officialSetsEqual(nextOfficial, officialSaved); + const canSaveOfficial = + officialDirty && !savingOfficial && !busy && !modelsBusy && hostReady; const policyMode = (detail.policy_mode || "").toLowerCase(); const currentPolicy = policyFromDetail(detail.policy_mode); @@ -1526,29 +1598,17 @@ export function AgentOwnerSettings({ model_id: displayedModelId, markup_percent: markupParsed, }) - .then(async (row) => { + .then((row) => { setPricingMsg(t.myAgentsPricingSaved); - setModelIdDraft(resolvePricingModelId(row)); + setModelIdDraft( + resolvePricingModelId( + row, + supportedModels, + row.official_models ?? officialSaved, + ), + ); const mu = row.token_pricing?.markup_percent; if (typeof mu === "number" && Number.isFinite(mu)) setMarkupDraft(String(mu)); - // Official is frozen. Saving OpenRouter / vendor pricing must drop - // leftover Host official authorization so listen stops injecting hops. - if (officialSaved.length > 0) { - try { - const cleared = await client.updateMyAgentOfficialModels( - detail.agent_id, - [], - ); - setOfficialSaved(cleared.model_ids); - row = { - ...row, - official_models: cleared.model_ids, - host_inference_ready: cleared.host_inference_ready, - }; - } catch { - // Pricing saved; leftover official_models stay until Host accepts []. - } - } window.setTimeout(() => setPricingMsg(null), 2000); onUpdated?.(row); return row; @@ -1562,7 +1622,7 @@ export function AgentOwnerSettings({ ? err.message.trim() : t.myAgentsPricingFailed; setPricingError(msg); - setModelIdDraft(resolvePricingModelId(detail, supportedModels)); + setModelIdDraft(resolvePricingModelId(detail, supportedModels, officialSaved)); return null; }) .finally(() => setSavingPricing(false)); @@ -1581,6 +1641,10 @@ export function AgentOwnerSettings({ .then((row) => { setOfficialSaved(row.model_ids); setHostReady(Boolean(row.host_inference_ready)); + if (row.official_key_geo) setOfficialKeyGeo(row.official_key_geo); + if (row.official_default_model_id) { + setOfficialDefaultModelId(row.official_default_model_id); + } setOfficialMsg(t.myAgentsProvidersSaved); window.setTimeout(() => setOfficialMsg(null), 2000); onUpdated?.({ @@ -1675,13 +1739,26 @@ export function AgentOwnerSettings({ if (typeof status.host_inference_ready === "boolean") { setHostReady(status.host_inference_ready); } + const official = Array.isArray(status.official_models) + ? status.official_models + : officialSaved; if (Array.isArray(status.official_models)) { setOfficialSaved(status.official_models); } const runtime = status.runtime_model_id || detail.runtime_model_id || ""; - setModelIdDraft(resolvePricingModelId(detail, ids)); + const ready = + typeof status.host_inference_ready === "boolean" + ? status.host_inference_ready + : hostReady; + setModelIdDraft(resolvePricingModelId(detail, ids, official)); setSettingsProvider( - providerIdFromRuntime(runtime, resolvePricingModelId(detail, ids), ids), + providerIdFromRuntime( + runtime, + resolvePricingModelId(detail, ids, official), + ids, + official, + ready, + ), ); onUpdated?.({ ...detail, @@ -1711,15 +1788,34 @@ export function AgentOwnerSettings({ if (openingPolicy) latest = (await runSavePolicy()) ?? latest; if (doDelivery) latest = (await runSaveDelivery()) ?? latest; if (otherPolicy) latest = (await runSavePolicy()) ?? latest; - if (doPricing) latest = (await runSavePricing()) ?? latest; - if (doOfficial) { - const officialRow = await runSaveOfficial(); - if (officialRow && latest) { - latest = { - ...latest, - official_models: officialRow.model_ids, - host_inference_ready: officialRow.host_inference_ready, - }; + const saveOfficialThenPricing = officialSelected && (doOfficial || doPricing); + if (saveOfficialThenPricing) { + let officialOk = !doOfficial; + if (doOfficial) { + const officialRow = await runSaveOfficial(); + officialOk = Boolean(officialRow); + if (officialRow && latest) { + latest = { + ...latest, + official_models: officialRow.model_ids, + host_inference_ready: officialRow.host_inference_ready, + }; + } + } + if (doPricing && officialOk) { + latest = (await runSavePricing()) ?? latest; + } + } else { + if (doPricing) latest = (await runSavePricing()) ?? latest; + if (doOfficial) { + const officialRow = await runSaveOfficial(); + if (officialRow && latest) { + latest = { + ...latest, + official_models: officialRow.model_ids, + host_inference_ready: officialRow.host_inference_ready, + }; + } } } if (latest) onUpdated?.(latest); @@ -2016,10 +2112,24 @@ export function AgentOwnerSettings({ onChange={(e) => { const next = e.target.value; setSettingsProvider(next); + if (next === OFFICIAL_OPENROUTER) { + const equiv = + findOfficialEquivalent(modelIdDraft, officialIds) || + officialSaved.find((id) => + officialIds.some((item) => sameModelId(item, id)), + ) || + (officialDefaultModelId + ? officialIds.find((id) => sameModelId(id, officialDefaultModelId)) + : undefined) || + officialIds[0]; + if (equiv) setModelIdDraft(equiv); + return; + } if (next === OPENROUTER_BYO) { - const equiv = findOfficialEquivalent(modelIdDraft, officialIds); + const equiv = + findOfficialEquivalent(modelIdDraft, byoOpenRouterIds) || + byoOpenRouterIds[0]; if (equiv) setModelIdDraft(equiv); - else if (officialIds[0]) setModelIdDraft(officialIds[0]); return; } const list = modelsForProvider(supportedModels, next); @@ -2043,6 +2153,17 @@ export function AgentOwnerSettings({ )} {officialSelected ? ( +

+ {t.myAgentsOfficialHostHint} +

+ ) : openRouterByoSelected ? (

{modelsBusy ? (

- ) : activeProvider === OPENROUTER_BYO ? ( + ) : officialSelected ? ( officialIds.length === 0 ? (

{t.myAgentsPricingOfficialEmpty} @@ -2207,6 +2328,29 @@ export function AgentOwnerSettings({ onChange={setModelIdDraft} /> ) + ) : openRouterByoSelected ? ( + byoOpenRouterIds.length === 0 ? ( +

+ {t.myAgentsPricingOfficialEmpty} +

+ ) : ( + + catalogOptionLabel( + id, + openRouterByoCatalog.find((row) => sameModelId(row.id, id)), + t.myAgentsPricingOptionLine, + ) + } + onChange={setModelIdDraft} + /> + ) ) : vendorModels.length === 0 ? (

{t.myAgentsPricingModelsEmpty} diff --git a/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx b/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx index a77fdee..91ede5c 100644 --- a/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx +++ b/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx @@ -2737,7 +2737,6 @@ export function RanchChatShell(props: RanchChatShellProps) { : (activeTopic?.id ?? composerTopic?.id ?? null); await client.sendMessage(chatId, text, mentions, sendThreadId, { requested_model: selectedModelId, - requested_provider: "byo", }); if (group && mentions) { if (mentions.length === 1) { @@ -2886,7 +2885,7 @@ export function RanchChatShell(props: RanchChatShellProps) { text, mentions, activeTopic?.id ?? composerTopic?.id ?? null, - { requested_model: selectedModelId, requested_provider: "byo" }, + { requested_model: selectedModelId }, ); if (group && mentions) { if (mentions.length === 1) { @@ -3098,9 +3097,10 @@ export function RanchChatShell(props: RanchChatShellProps) { const officialIds = Array.isArray(row.official_models) ? row.official_models.filter((m): m is string => typeof m === "string" && !!m.trim()) : []; - // Official hop is frozen (P7). Leftover official_models must not - // switch the composer or send path back to Host-held keys. - const listedOfficial = false; + const listedOfficial = + row.host_inference_ready !== false && + !!listed && + officialIds.some((id) => sameModelId(id, listed)); setComposerModel({ listed_model_id: listed, runtime_model_id: runtime, @@ -4797,7 +4797,10 @@ export function RanchChatShell(props: RanchChatShellProps) { : [listed].filter(Boolean); const seen = new Set(); const out: string[] = []; - for (const id of [listed, ...fromApi]) { + // Official shelf stays in Settings. Chat lists machine self-report + // (plus the listing id only when it is a BYO/Store SKU). + const source = official ? fromApi : [listed, ...fromApi]; + for (const id of source) { if (!id || !id.trim()) continue; const k = id.toLowerCase(); if (seen.has(k)) continue; @@ -4809,6 +4812,7 @@ export function RanchChatShell(props: RanchChatShellProps) { const visible = filterComposerModels(options, composerMenuQuery); const value = options.find((id) => sameModelId(id, selectedModelId)) || + (official && selectedModelId ? selectedModelId : "") || options.find((id) => sameModelId(id, listed)) || options[0] || ""; diff --git a/packages/agent-chat/src/ranch-shell/i18n.ts b/packages/agent-chat/src/ranch-shell/i18n.ts index 6c2847f..940ef89 100644 --- a/packages/agent-chat/src/ranch-shell/i18n.ts +++ b/packages/agent-chat/src/ranch-shell/i18n.ts @@ -364,6 +364,7 @@ export type RanchMessages = { myAgentsProviderOther: string; myAgentsProviderOfficialOpenRouter: string; myAgentsProviderOpenRouter: string; + myAgentsOfficialHostHint: string; myAgentsNeedStoreKey: string; myAgentsOpenRouterRuntimeRequired: string; myAgentsBuyStoreKey: string; @@ -912,10 +913,10 @@ const en: RanchMessages = { myAgentsInferencePathByoHint: "This agent calls models with its own key. Usage is self-reported — Host cannot verify. Official hosted inference is not available yet.", myAgentsInferencePathByoHintReady: - "Chat picks a model. OpenRouter here is a Store key on the agent — not Interfaze’s official hop.", + "Chat picks a model. Official · OpenRouter is Interfaze’s key. OpenRouter here is a Store key on the agent.", myAgentsProviderLabel: "Provider", myAgentsProviderHint: - "Provider follows the agent runtime. Host cannot see the key — only the heartbeat model. OpenRouter can be saved only after the agent reports an OpenRouter model (write a Store key, then Refresh status). Catalog prefixes are not providers.", + "Official · OpenRouter is Interfaze’s Host-held key (this Host’s shelf). OpenRouter is a Store/BYO key on the agent. Chat still picks a model only — path is inferred. BYO vendors follow the heartbeat.", myAgentsProviderByo: "Mine", myAgentsProviderOfficial: "Official · OpenRouter", myAgentsProviderMine: "Mine", @@ -923,6 +924,8 @@ const en: RanchMessages = { myAgentsProviderOther: "Other", myAgentsProviderOfficialOpenRouter: "Official · OpenRouter", myAgentsProviderOpenRouter: "OpenRouter", + myAgentsOfficialHostHint: + "Official hops use Interfaze’s Host OpenRouter key. Quotes are published Catalog, not Store sync. This Host’s CN-billed key cannot complete GPT, Claude, or Gemini. The machine default stays on the agent’s own key.", myAgentsNeedStoreKey: "OpenRouter needs a key on this agent. Host cannot see the key — we only read the heartbeat model. If it doesn’t have one yet, buy Store credits, write the key into the runtime, then Refresh status.", myAgentsOpenRouterRuntimeRequired: @@ -932,7 +935,7 @@ const en: RanchMessages = { myAgentsListingStaleHint: "The published listing still says OpenRouter, but this agent is running its own key. That’s leftover — not a real OpenRouter setup. OpenRouter can be saved only after the agent reports an OpenRouter model.", myAgentsOfficialSectionHint: - "Official hosted inference is frozen. OpenRouter here is a Store/BYO key on the agent — not Interfaze’s key.", + "Official · OpenRouter uses Interfaze’s Host-held key and this Host’s shelf. OpenRouter is Store/BYO on the agent.", myAgentsMineSectionHint: "These models stay on this agent’s own key. Provider follows the live runtime.", myAgentsProviderUnlisted: "Authorized, not in this agent’s self-report yet — won’t appear in chat until it reports the model.", @@ -1486,10 +1489,10 @@ const zh: RanchMessages = { myAgentsInferencePathByoHint: "这只 agent 用自己的钥匙打模型。用量自报,平台未核验。官方代打尚未开放。", myAgentsInferencePathByoHintReady: - "聊天只选模型。这里的 OpenRouter 是写在 agent 上的 Store 钥匙,不是 Interfaze 官方代打。", + "聊天只选模型。「官方 · OpenRouter」是 Interfaze 持钥。这里的 OpenRouter 是写在 agent 上的 Store 钥匙。", myAgentsProviderLabel: "供应商", myAgentsProviderHint: - "供应商跟 agent 运行时走。Host 看不见钥匙,只认心跳型号。只有它自报了 OpenRouter 型号(写入 Store 钥匙后点「刷新状态」)才能保存。型号 id 里的货架前缀不是供应商。", + "「官方 · OpenRouter」是 Interfaze 持钥、本 Host 货架。「OpenRouter」是写在 agent 上的 Store/自持钥匙。聊天只选模型,路径由挂牌推断。自持厂家跟心跳走。", myAgentsProviderByo: "我的", myAgentsProviderOfficial: "官方 · OpenRouter", myAgentsProviderMine: "我的", @@ -1497,6 +1500,8 @@ const zh: RanchMessages = { myAgentsProviderOther: "其他", myAgentsProviderOfficialOpenRouter: "官方 · OpenRouter", myAgentsProviderOpenRouter: "OpenRouter", + myAgentsOfficialHostHint: + "官方跳走 Interfaze 持钥,报价是对外 Catalog,不是 Store 同步价。本 Host 国内卡账户打不了 GPT / Claude / Gemini。机器默认型号仍在 agent 自持钥上。", myAgentsNeedStoreKey: "OpenRouter 需要这只 agent 自己的钥匙。Host 看不见钥匙,只认心跳型号。还没有的话,去 Store 买额度、写入 runtime,再点「刷新状态」。", myAgentsOpenRouterRuntimeRequired: @@ -1506,7 +1511,7 @@ const zh: RanchMessages = { myAgentsListingStaleHint: "挂牌还写着 OpenRouter,但这只 agent 正在用自己的钥匙。那是旧挂牌,不是真的 OpenRouter。只有它自报了 OpenRouter 型号才能保存。", myAgentsOfficialSectionHint: - "官方代打已冻结。这里的 OpenRouter 是写在 agent 上的 Store/自持钥匙,不是 Interfaze 持钥。", + "「官方 · OpenRouter」走 Interfaze 持钥、本 Host 货架。「OpenRouter」是写在 agent 上的 Store/自持钥匙。", myAgentsMineSectionHint: "下列模型走这只 agent 自己的钥匙。供应商跟运行时走。", myAgentsProviderUnlisted: "已授权,但 agent 尚未自报——写进自报之前不会出现在聊天下拉。", diff --git a/packages/agent-chat/src/ranch-shell/officialV0.ts b/packages/agent-chat/src/ranch-shell/officialV0.ts index cb0ec31..b30f6c8 100644 --- a/packages/agent-chat/src/ranch-shell/officialV0.ts +++ b/packages/agent-chat/src/ranch-shell/officialV0.ts @@ -2,6 +2,9 @@ * Keep in lockstep with backend official_v0_supports_model and ACN CLI. */ const O_SERIES = /(^|\/)o[134](?:$|[-/:])/; +/** CN-billed OpenRouter keys reject these prefixes. Match Host official_shelf_allows. */ +const CN_KEY_BLOCKED_PREFIXES = ["openai/", "anthropic/", "google/gemini"] as const; + export function officialV0SupportsModel(modelId: string | null | undefined): boolean { const id = (modelId || "").trim().toLowerCase(); if (!id) return true; @@ -11,3 +14,14 @@ export function officialV0SupportsModel(modelId: string | null | undefined): boo if (id.includes("deepseek-r1")) return false; return !O_SERIES.test(id); } + +/** Settings Official shelf: this Host key can complete this id. */ +export function officialShelfAllows( + modelId: string | null | undefined, + keyGeo: string | null | undefined, +): boolean { + const id = (modelId || "").trim().toLowerCase(); + if (!id || !officialV0SupportsModel(id)) return false; + if ((keyGeo || "").trim().toLowerCase() !== "cn") return true; + return !CN_KEY_BLOCKED_PREFIXES.some((prefix) => id.startsWith(prefix)); +}