This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
The following commit(s) were added to refs/heads/master by this push:
new d5a47c3 fix(agent): name the model an unbound agent falls back to
d5a47c3 is described below
commit d5a47c3b2110d691033250ee0f0339385fcdb3b9
Author: Yang Luo <[email protected]>
AuthorDate: Tue Aug 25 17:33:12 2026 +0800
fix(agent): name the model an unbound agent falls back to
---
agentprovider/claude_code.go | 44 ++++++++++++++++++-----
agentprovider/codex.go | 68 ++++++++++++++++++++++++++----------
agentprovider/dsh.go | 24 +++++++++++++
agentprovider/opencode.go | 22 ++++++++++++
agentprovider/provider.go | 46 +++++++++++++++++++++---
object/agent.go | 12 +++++--
web/src/components/AgentGridCard.tsx | 16 +++++----
web/src/components/ProviderCard.tsx | 38 +++++++++++++++-----
web/src/lib/agents.ts | 15 ++++++--
web/src/locales/en/data.json | 8 +++--
web/src/locales/zh/data.json | 8 +++--
web/src/pages/AgentsPage.tsx | 5 ++-
web/src/types.ts | 4 +++
13 files changed, 252 insertions(+), 58 deletions(-)
diff --git a/agentprovider/claude_code.go b/agentprovider/claude_code.go
index b877cbd..635d278 100644
--- a/agentprovider/claude_code.go
+++ b/agentprovider/claude_code.go
@@ -16,10 +16,15 @@ package agentprovider
import (
"path/filepath"
+ "strings"
"github.com/apache/casbin-gateway/agenthome"
)
+// claudeCodeBuiltin is what Claude Code talks to with no endpoint of its own:
+// the Anthropic sign-in, whose model the CLI picks by itself.
+const claudeCodeBuiltin = "Claude"
+
// The env keys Gateway owns in settings.json. ANTHROPIC_API_KEY is one of them
// even though a switch never sets it: a key left over from another provider
// would otherwise be sent to the new one.
@@ -134,20 +139,43 @@ func (w claudeCodeWriter) Restore(target Target, previous
map[string]string) err
}
func (w claudeCodeWriter) Current(target Target) (string, error) {
+ env, err := w.envOf(target)
+ if err != nil || env == nil {
+ return "", err
+ }
+ baseUrl, _ := env["ANTHROPIC_BASE_URL"].(string)
+ return baseUrl, nil
+}
+
+func (w claudeCodeWriter) Builtin(target Target, previous map[string]string)
string {
+ if previous != nil {
+ return emptyAs(previous["ANTHROPIC_MODEL"], claudeCodeBuiltin)
+ }
+
+ env, err := w.envOf(target)
+ if err != nil || env == nil {
+ return claudeCodeBuiltin
+ }
+ // A model beside an endpoint Gateway wrote is Gateway's, not the one
Claude
+ // Code would pick for itself.
+ if baseUrl, _ := env["ANTHROPIC_BASE_URL"].(string);
strings.Contains(baseUrl, "/v1/agents/") {
+ return claudeCodeBuiltin
+ }
+ model, _ := env["ANTHROPIC_MODEL"].(string)
+ return emptyAs(model, claudeCodeBuiltin)
+}
+
+// envOf is the env block of settings.json, nil when the file has none.
+func (w claudeCodeWriter) envOf(target Target) (map[string]any, error) {
path, err := w.configPath(target)
if err != nil {
- return "", err
+ return nil, err
}
config, _, err := readJSON(path)
if err != nil {
- return "", err
- }
- env := objectAt(config, "env")
- if env == nil {
- return "", nil
+ return nil, err
}
- baseUrl, _ := env["ANTHROPIC_BASE_URL"].(string)
- return baseUrl, nil
+ return objectAt(config, "env"), nil
}
// env is the block written into settings.json. Claude Code authenticates with
diff --git a/agentprovider/codex.go b/agentprovider/codex.go
index e6537f7..df21686 100644
--- a/agentprovider/codex.go
+++ b/agentprovider/codex.go
@@ -33,6 +33,9 @@ const (
// through ChatGPT got "Missing environment variable: OPENAI_API_KEY"
// instead, so the key now travels in the header below.
codexAuthKey = "OPENAI_API_KEY"
+ // codexBuiltin is what Codex talks to with no provider entry of its
own: the
+ // ChatGPT sign-in, whose model the CLI picks by itself.
+ codexBuiltin = "ChatGPT"
// codexAuthHeader carries the key on every request Codex sends to the
// provider, which is the one place Codex reads it from without an
// environment variable and without a sign-in of its own.
@@ -125,14 +128,16 @@ func (w codexWriter) Apply(target Target, endpoint
Endpoint) (map[string]string,
}
previous := map[string]string{codexHeaderStateKey: "1"}
- // The CLI and the VS Code integration share one ~/.codex, so the
selection
- // found here can be Gateway's own from the other one, which is not
what a
- // restore should put back.
- if value, ok := document[codexModelProviderKey].(string); ok && value
!= codexProviderName {
- previous[codexModelProviderKey] = value
- }
- if value, ok := document[codexModelKey].(string); ok {
- previous[codexModelKey] = value
+ // The CLI and the VS Code integration share one ~/.codex, so what is
found
+ // here can be Gateway's own selection from the other one, model
included,
+ // which is not what a restore should put back.
+ if selected, _ := document[codexModelProviderKey].(string); selected !=
codexProviderName {
+ if selected != "" {
+ previous[codexModelProviderKey] = selected
+ }
+ if value, ok := document[codexModelKey].(string); ok {
+ previous[codexModelKey] = value
+ }
}
text := tomlCutTable(string(data), codexProviderPath...)
@@ -186,8 +191,10 @@ func (w codexWriter) Restore(target Target, previous
map[string]string) error {
}
// Only a state from the release that put the key in auth.json has one
to
- // take back out; a newer one never opened the file.
- if _, inHeader := previous[codexHeaderStateKey]; !inHeader {
+ // take back out; a newer one, and a restore with no state at all, never
+ // opened the file.
+ _, inHeader := previous[codexHeaderStateKey]
+ if previous != nil && !inHeader {
auth, _, err := readJSON(authPath)
if err != nil {
changes.abort()
@@ -212,15 +219,7 @@ func (w codexWriter) Restore(target Target, previous
map[string]string) error {
}
func (w codexWriter) Current(target Target) (string, error) {
- configPath, _, err := w.paths(target)
- if err != nil {
- return "", err
- }
- data, _, exists, err := readFile(configPath)
- if err != nil || !exists {
- return "", err
- }
- document, err := w.decode(configPath, data)
+ document, err := w.document(target)
if err != nil {
return "", err
}
@@ -239,6 +238,37 @@ func (w codexWriter) Current(target Target) (string,
error) {
return selected, nil
}
+func (w codexWriter) Builtin(target Target, previous map[string]string) string
{
+ if previous != nil {
+ return emptyAs(previous[codexModelKey], codexBuiltin)
+ }
+
+ document, err := w.document(target)
+ if err != nil {
+ return codexBuiltin
+ }
+ // A model beside Gateway's own provider entry is Gateway's, not the one
+ // Codex would pick for itself.
+ if selected, _ := document[codexModelProviderKey].(string); selected ==
codexProviderName {
+ return codexBuiltin
+ }
+ model, _ := document[codexModelKey].(string)
+ return emptyAs(model, codexBuiltin)
+}
+
+// document is config.toml parsed, empty when the file is not there yet.
+func (w codexWriter) document(target Target) (map[string]any, error) {
+ configPath, _, err := w.paths(target)
+ if err != nil {
+ return nil, err
+ }
+ data, _, exists, err := readFile(configPath)
+ if err != nil || !exists {
+ return map[string]any{}, err
+ }
+ return w.decode(configPath, data)
+}
+
// check reports why Codex cannot be pointed at endpoint.
func (codexWriter) check(endpoint Endpoint) error {
if endpoint.ApiKey == "" {
diff --git a/agentprovider/dsh.go b/agentprovider/dsh.go
index f8e1cd0..62bc2b1 100644
--- a/agentprovider/dsh.go
+++ b/agentprovider/dsh.go
@@ -37,6 +37,9 @@ const (
// dshProtocol is the wire format the route serves. The gateway answers
// OpenAI chat completions under the base URL dsh is pointed at.
dshProtocol = "openai-completions"
+ // dshBuiltin is what dsh talks to with no route of its own: the
DeepSeek
+ // service it ships with.
+ dshBuiltin = "DeepSeek"
)
// The settings Gateway overwrites, remembered under these names.
@@ -246,6 +249,27 @@ func (w dshWriter) Current(target Target) (string, error) {
return selected, nil
}
+func (w dshWriter) Builtin(target Target, previous map[string]string) string {
+ if previous != nil {
+ return emptyAs(previous[dshModelKey], dshBuiltin)
+ }
+
+ settingsPath, _, err := w.paths(target)
+ if err != nil {
+ return dshBuiltin
+ }
+ _, root, err := w.load(settingsPath)
+ if err != nil {
+ return dshBuiltin
+ }
+ // A model beside Gateway's own route is Gateway's, not the one dsh
would
+ // pick for itself.
+ if yamledit.String(root, "agent-default-model", "provider") == dshRoute
{
+ return dshBuiltin
+ }
+ return emptyAs(yamledit.String(root, "agent-default-model", "model"),
dshBuiltin)
+}
+
// check reports why dsh cannot be pointed at endpoint.
func (dshWriter) check(endpoint Endpoint) error {
if endpoint.ApiKey == "" {
diff --git a/agentprovider/opencode.go b/agentprovider/opencode.go
index 0e37276..cb182c0 100644
--- a/agentprovider/opencode.go
+++ b/agentprovider/opencode.go
@@ -166,6 +166,28 @@ func (w opencodeWriter) Current(target Target) (string,
error) {
return selected, nil
}
+// Builtin is the model opencode selects for itself. opencode ships no provider
+// of its own, so a file that selects none has no name to show.
+func (w opencodeWriter) Builtin(target Target, previous map[string]string)
string {
+ if previous != nil {
+ return previous[opencodeModelKey]
+ }
+
+ path, err := w.configPath(target)
+ if err != nil {
+ return ""
+ }
+ config, err := w.load(path)
+ if err != nil {
+ return ""
+ }
+ model := stringAt(config, opencodeModelKey)
+ if strings.HasPrefix(model, opencodeProvider+"/") {
+ return ""
+ }
+ return model
+}
+
// check reports why opencode cannot be pointed at endpoint.
func (opencodeWriter) check(endpoint Endpoint) error {
if endpoint.ApiKey == "" {
diff --git a/agentprovider/provider.go b/agentprovider/provider.go
index 5f3a220..451f4c4 100644
--- a/agentprovider/provider.go
+++ b/agentprovider/provider.go
@@ -19,6 +19,7 @@ package agentprovider
import (
"errors"
"fmt"
+ "strings"
"sync"
)
@@ -81,6 +82,13 @@ type Status struct {
Time string `json:"time"`
Files []string `json:"files"`
Detail string `json:"detail"`
+ // Builtin is what the agent talks to on its own, with nothing bound:
the
+ // model its own configuration names, or the account it signs in to
when it
+ // names none.
+ Builtin string `json:"builtin"`
+ // Current is the endpoint the agent's files name right now, whichever
tool
+ // wrote them.
+ Current string `json:"current"`
}
type writer interface {
@@ -96,6 +104,10 @@ type writer interface {
// Current is the base URL the files point at right now, empty when the
// agent has no provider configured.
Current(Target) (string, error)
+ // Builtin names the model the agent uses without Gateway. previous is
what
+ // the files held before Gateway first wrote them, nil when it never
did and
+ // the files themselves are read instead.
+ Builtin(Target, map[string]string) string
}
var (
@@ -169,18 +181,28 @@ func Restore(target Target) error {
writerMutex.Lock()
defer writerMutex.Unlock()
- // An installation Gateway never switched has nothing to put back,
whether
- // or not its format is one Gateway can write.
saved, err := loadState(target)
- if err != nil || saved == nil {
+ if err != nil {
return err
}
value, ok := writers[target.AgentId]
if !ok {
+ // An installation Gateway never switched has nothing to put
back,
+ // whether or not its format is one Gateway can write.
+ if saved == nil {
+ return nil
+ }
return fmt.Errorf("%s: %w", target.AgentId, ErrNotSupported)
}
- if err := value.Restore(target, saved.Previous); err != nil {
+
+ var previous map[string]string
+ if saved != nil {
+ previous = saved.Previous
+ } else if !pointsAtGateway(value, target) {
+ return nil
+ }
+ if err := value.Restore(target, previous); err != nil {
return err
}
return clearState(target)
@@ -205,11 +227,18 @@ func StatusOf(target Target) Status {
return status
}
+ if saved == nil {
+ status.Builtin = value.Builtin(target, nil)
+ } else {
+ status.Builtin = value.Builtin(target, saved.Previous)
+ }
+
current, err := value.Current(target)
if err != nil {
status.Detail = err.Error()
return status
}
+ status.Current = current
if saved == nil {
if current == "" {
@@ -272,6 +301,15 @@ func writerFor(target Target, endpoint Endpoint) (writer,
error) {
return value, nil
}
+// pointsAtGateway reports whether the agent configuration still names the
local
+// proxy. A state file can be lost while the file it wrote stays behind, and
+// putting back only what state records would leave the agent calling a proxy
+// with no provider behind it.
+func pointsAtGateway(value writer, target Target) bool {
+ current, err := value.Current(target)
+ return err == nil && strings.Contains(current, "/v1/agents/")
+}
+
func emptyAs(value string, fallback string) string {
if value == "" {
return fallback
diff --git a/object/agent.go b/object/agent.go
index 7c9065e..f1577b9 100644
--- a/object/agent.go
+++ b/object/agent.go
@@ -184,7 +184,7 @@ func GetProvidersByAgent(agentId string) ([]*Provider,
error) {
return nil, err
}
if agent == nil || agent.Provider == "" {
- return nil, fmt.Errorf("%w: %s", ErrAgentNoProvider, agentId)
+ return nil, errNoProvider(agentId)
}
providers := []*Provider{}
@@ -208,13 +208,21 @@ func GetProvidersByAgent(agentId string) ([]*Provider,
error) {
if len(providers) == 0 {
if skipped == "" {
- return nil, fmt.Errorf("%w: %s", ErrAgentNoProvider,
agentId)
+ return nil, errNoProvider(agentId)
}
return nil, errors.New(skipped)
}
return providers, nil
}
+// errNoProvider says what an unbound agent should do instead of calling the
+// proxy: an agent left on its own built-in model has no reason to reach
Gateway
+// at all, so its configuration is what needs putting back.
+func errNoProvider(agentId string) error {
+ return fmt.Errorf("%w: %s. It is set to its own built-in model, so bind
a provider, or restore its configuration so it stops calling Gateway",
+ ErrAgentNoProvider, agentId)
+}
+
// GetProviderByAgent resolves the first provider of an agent's chain.
func GetProviderByAgent(agentId string) (*Provider, error) {
providers, err := GetProvidersByAgent(agentId)
diff --git a/web/src/components/AgentGridCard.tsx
b/web/src/components/AgentGridCard.tsx
index 05b4b7f..649f66a 100644
--- a/web/src/components/AgentGridCard.tsx
+++ b/web/src/components/AgentGridCard.tsx
@@ -35,8 +35,10 @@ import {
import {Switch} from "@/components/ui/switch";
import {SimpleTooltip} from "@/components/ui/tooltip";
import {
+ agentBuiltin,
agentCanUse,
agentDetailPath,
+ builtinProvider,
directMode,
monitorAgentId,
type AgentActivity,
@@ -52,9 +54,6 @@ import type {
ProviderQuota,
} from "@/types";
-/** Radix rejects an empty item value, so "unbound" needs a stand-in. */
-const noProvider = "-";
-
/** One number of the card, and what it is. */
function Metric({
label,
@@ -215,16 +214,19 @@ export function AgentGridCard({
</div>
<Select
- value={agent.provider === "" ? noProvider : agent.provider}
+ value={agent.provider === "" ? builtinProvider : agent.provider}
disabled={busy}
- onValueChange={value => onEnable(value === noProvider ? "" :
value)}
+ onValueChange={value => onEnable(value === builtinProvider ? "" :
value)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
- <SelectItem value={noProvider}>
- <span className="text-muted-foreground">{i18next.t("agent:No
provider")}</span>
+ <SelectItem value={builtinProvider}>
+ {agentBuiltin(agent)}
+ <span className="text-muted-foreground ml-2 text-xs">
+ {i18next.t("agent:Built-in")}
+ </span>
</SelectItem>
{options.map(provider => (
<SelectItem key={providerIdOf(provider)}
value={providerIdOf(provider)}>
diff --git a/web/src/components/ProviderCard.tsx
b/web/src/components/ProviderCard.tsx
index 8a1340d..3069317 100644
--- a/web/src/components/ProviderCard.tsx
+++ b/web/src/components/ProviderCard.tsx
@@ -42,9 +42,11 @@ import {
} from "@/components/ui/select";
import {Switch} from "@/components/ui/switch";
import {
+ agentBuiltin,
agentNeedsResponsesApi,
agentProxyBaseUrl,
agentSetupNoteKey,
+ builtinProvider,
directMode,
gatewayMode,
} from "@/lib/agents";
@@ -58,9 +60,6 @@ import {
import {cn} from "@/lib/utils";
import type {Agent, AgentProviderFile, Provider, ProviderHealth} from
"@/types";
-/** Radix rejects an empty item value, so "unbound" needs a stand-in. */
-const noProvider = "-";
-
/** The file preview, loaded when the dialog opens rather than on every
render. */
function PreviewDialog({
agent,
@@ -196,6 +195,8 @@ export function ProviderCard({
time: "",
files: [],
detail: "",
+ builtin: "",
+ current: "",
};
const noteKey = agentSetupNoteKey(agent.agentId);
const healthOf = (id: string) => health.find(item => item.provider === id);
@@ -225,6 +226,11 @@ export function ProviderCard({
// directly, so the choice is not offered rather than failing on the write.
const gatewayOnly = agentNeedsResponsesApi(agent.agentId) &&
!servesResponsesApi(bound);
+ // Nothing is bound, yet the agent still calls the proxy: it gets an error
+ // back on every request until its own configuration is put back.
+ const stranded =
+ agent.provider === "" && (providerConfig.current ??
"").includes("/v1/agents/");
+
const toggleFallback = (id: string) => {
onRouting({
fallbacks: fallbacks.includes(id)
@@ -240,16 +246,21 @@ export function ProviderCard({
</CardHeader>
<CardContent className="space-y-3 p-4 pt-0">
<Select
- value={agent.provider === "" ? noProvider : agent.provider}
+ value={agent.provider === "" ? builtinProvider : agent.provider}
disabled={busy}
- onValueChange={value => onRouting({provider: value === noProvider ?
"" : value, fallbacks: []})}
+ onValueChange={value =>
+ onRouting({provider: value === builtinProvider ? "" : value,
fallbacks: []})
+ }
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
- <SelectItem value={noProvider}>
- <span className="text-muted-foreground">{i18next.t("agent:No
provider")}</span>
+ <SelectItem value={builtinProvider}>
+ {agentBuiltin(agent)}
+ <span className="ml-2 text-xs text-muted-foreground">
+ {i18next.t("agent:Built-in")}
+ </span>
</SelectItem>
{options.map(provider => (
<SelectItem key={providerIdOf(provider)}
value={providerIdOf(provider)}>
@@ -274,8 +285,17 @@ export function ProviderCard({
</p>
) : null}
- {agent.provider === "" ? (
- <p className="text-sm
text-muted-foreground">{i18next.t("agent:Provider hint")}</p>
+ {stranded ? (
+ <div className="space-y-2 rounded-md border p-3">
+ <p className="text-sm text-warning">{i18next.t("agent:Still
calling Gateway")}</p>
+ <code className="block truncate
text-xs">{providerConfig.current}</code>
+ <Button variant="outline" disabled={busy} onClick={() =>
onWrite(true)}>
+ <RotateCcw />
+ {i18next.t("agent:Restore configuration")}
+ </Button>
+ </div>
+ ) : agent.provider === "" ? (
+ <p className="text-sm
text-muted-foreground">{i18next.t("agent:Built-in hint")}</p>
) : bound === undefined ? null : (
<>
<div className="space-y-2">
diff --git a/web/src/lib/agents.ts b/web/src/lib/agents.ts
index 77b78dc..25c807d 100644
--- a/web/src/lib/agents.ts
+++ b/web/src/lib/agents.ts
@@ -24,10 +24,21 @@ import type {Agent, AgentRuntime, AgentSession, Provider}
from "@/types";
/** How long a started app is given before its process is looked for again. */
const runtimeSettleMs = 2000;
+/** Radix rejects an empty item value, so "the agent's own model" needs one. */
+export const builtinProvider = "-";
+
/** Routing through the local proxy, which is what makes a switch hot. */
export const gatewayMode = "gateway";
export const directMode = "direct";
+/**
+ * What the agent talks to with nothing bound: the model its own configuration
+ * names, or the service it signs in to. Binding nothing puts it back on this.
+ */
+export function agentBuiltin(agent: Agent) {
+ return agent.providerConfig?.builtin || i18next.t("agent:Built-in model");
+}
+
/** The monitor keys the two Codex front ends under one agent id. */
export function monitorAgentId(agentId: string) {
return agentId === "codex_vscode" || agentId === "codex-vscode" ?
"codex-cli" : agentId;
@@ -277,7 +288,7 @@ export function useAgents(enabled = true) {
Setting.showMessage(
"success",
next.provider === ""
- ? i18next.t("agent:Provider cleared")
+ ? `${i18next.t("agent:Built-in model restored")}:
${agentBuiltin(agent)}`
: `${i18next.t("agent:Provider saved")}: ${next.provider}`,
);
scan();
@@ -328,7 +339,7 @@ export function useAgents(enabled = true) {
Setting.showMessage(
"success",
providerId === ""
- ? i18next.t("agent:Provider cleared")
+ ? `${i18next.t("agent:Built-in model restored")}:
${agentBuiltin(agent)}`
: `${i18next.t("agent:Provider enabled")}: ${providerId}`,
);
})
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 1dbcba5..d60db29 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -36,6 +36,10 @@
"Auto refresh": "Auto refresh",
"Base URL hint": "Give the agent the settings below. Requests that arrive
at this URL always go to the provider selected above, whatever model they
name.",
"Bound provider cooling down": "The bound provider is out of rotation,
requests go to the fallbacks",
+ "Built-in": "Built-in",
+ "Built-in hint": "This agent is on the model it comes with. Gateway
forwards nothing for it and only watches what it does.",
+ "Built-in model": "Built-in model",
+ "Built-in model restored": "Put back on its own model",
"Claude Code config hint": "The same variables can live in the \"env\"
block of ~/.claude/settings.json, so every session picks them up without
exporting anything.",
"Client auth token hint": "This provider forwards the agent's own
credentials, so no token is set: the agent keeps the sign-in it already has.",
"Codex CLI config hint": "The base URL can also be a model provider in
~/.codex/config.toml, selected with model_provider. Its wire_api has to be
\"responses\", the only wire format Codex still speaks.",
@@ -85,7 +89,6 @@
"No agent sessions yet": "No agent sessions yet",
"No agent sessions yet - patch an agent to start collecting them": "No
sessions yet - turn on monitoring for an agent to start collecting them",
"No other provider speaks this API": "No other provider speaks this API",
- "No provider": "No provider",
"No supported agents found": "No supported agents found",
"No usage yet": "No requests have been relayed for this agent yet",
"Not patched": "Not monitored",
@@ -102,9 +105,7 @@
"Processes": "Processes",
"Prompt ID": "Prompt ID",
"Provider": "Provider",
- "Provider cleared": "Provider cleared",
"Provider enabled": "Provider enabled",
- "Provider hint": "With no provider selected, this agent still talks to its
own upstream and Gateway only watches it.",
"Provider saved": "Provider saved",
"Provider speaks another API": "This agent speaks the {protocol} API,
which the bound provider does not. A direct binding needs both to speak the
same one: pick another provider, or route this agent through the gateway, which
translates.",
"Records": "Records",
@@ -126,6 +127,7 @@
"Start": "Start",
"Start app hint": "Opens the app on the machine Gateway runs on.",
"Start console hint": "Opens a console window running the CLI on the
machine Gateway runs on.",
+ "Still calling Gateway": "This agent is set to its own model, but its
configuration still points at Gateway, so every request fails. Put the
configuration back.",
"Stop": "Stop",
"Stop hint": "Ends every process of this installation, losing whatever it
is working on.",
"Stopped": "Stopped",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index b784b39..0561c1d 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -36,6 +36,10 @@
"Auto refresh": "自动刷新",
"Base URL hint": "把下面的设置配给该 Agent。到达这个地址的请求一律转发到上面选定的供应商,与请求里的模型名无关。",
"Bound provider cooling down": "绑定的供应商已暂时移出轮转,请求将走备用供应商",
+ "Built-in": "自带",
+ "Built-in hint": "该 Agent 正在使用自带的模型,网关不为它转发请求,只做观测。",
+ "Built-in model": "自带模型",
+ "Built-in model restored": "已恢复为自带模型",
"Claude Code config hint": "这两个变量也可以写进 ~/.claude/settings.json 的 env
段,这样不用每次 export,所有会话都会生效。",
"Client auth token hint": "该供应商转发 Agent 自己的凭据,所以不设置 token,Agent
继续用它已有的登录。",
"Codex CLI config hint": "base URL 也可以写成 ~/.codex/config.toml 里的一个 model
provider,再用 model_provider 选中它。它的 wire_api 必须是 \"responses\",这是 Codex
现在唯一还支持的线格式。",
@@ -85,7 +89,6 @@
"No agent sessions yet": "还没有会话",
"No agent sessions yet - patch an agent to start collecting them":
"还没有会话,开启某个智能体的监控后才会开始收集",
"No other provider speaks this API": "没有其他供应商使用相同的 API 协议",
- "No provider": "未设置供应商",
"No supported agents found": "没有发现本机安装的智能体",
"No usage yet": "该 Agent 还没有经过网关的请求",
"Not patched": "未开启",
@@ -102,9 +105,7 @@
"Processes": "进程",
"Prompt ID": "Prompt ID",
"Provider": "供应商",
- "Provider cleared": "已清除供应商",
"Provider enabled": "已启用供应商",
- "Provider hint": "未选择供应商时,该 Agent 仍直连自己的上游,网关只做观测。",
"Provider saved": "供应商已保存",
"Provider speaks another API": "该 Agent 使用 {protocol}
API,而绑定的供应商不支持。直连模式要求两者使用同一种 API:请改选其他供应商,或将该 Agent 改为经网关转发(网关会自动转换)。",
"Records": "记录数",
@@ -126,6 +127,7 @@
"Start": "启动",
"Start app hint": "在运行 Gateway 的机器上打开该应用。",
"Start console hint": "在运行 Gateway 的机器上打开一个控制台窗口来运行该命令行。",
+ "Still calling Gateway": "该 Agent
已设为使用自带模型,但它的配置仍指向网关,所以每个请求都会失败。请恢复它的配置。",
"Stop": "停止",
"Stop hint": "结束该安装的所有进程,正在进行的工作会丢失。",
"Stopped": "未运行",
diff --git a/web/src/pages/AgentsPage.tsx b/web/src/pages/AgentsPage.tsx
index 12785ff..0ace95a 100644
--- a/web/src/pages/AgentsPage.tsx
+++ b/web/src/pages/AgentsPage.tsx
@@ -28,6 +28,7 @@ import {Badge} from "@/components/ui/badge";
import {Button} from "@/components/ui/button";
import {SimpleTooltip} from "@/components/ui/tooltip";
import {
+ agentBuiltin,
agentDetailPath,
agentKey,
directMode,
@@ -124,7 +125,9 @@ export default function AgentsPage({account}: {account:
Account}) {
</span>
</div>
) : (
- <span className="text-muted-foreground">{i18next.t("agent:No
provider")}</span>
+ <SimpleTooltip title={i18next.t("agent:Built-in hint")}>
+ <span
className="text-muted-foreground">{agentBuiltin(record)}</span>
+ </SimpleTooltip>
),
},
{
diff --git a/web/src/types.ts b/web/src/types.ts
index f63fc49..1542d9f 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -219,6 +219,10 @@ export interface AgentProviderConfig {
time: string;
files: string[];
detail: string;
+ /** The model the agent uses on its own, or the account it signs in to. */
+ builtin: string;
+ /** The endpoint the agent's files name now, whichever tool wrote them. */
+ current: string;
}
export interface Agent {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]