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]

Reply via email to