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

commit 40ab857df92dde247aedcb1ff2e2a073f256dff9
Author: Yang Luo <[email protected]>
AuthorDate: Sun Sep 6 17:33:42 2026 +0800

    feat(agents): drive an agent through the provider bound to it
---
 agentprovider/claude_code.go             |  12 +++
 agentprovider/gemini.go                  |   6 ++
 agentprovider/provider.go                |  17 +++++
 agentprovider/qwen.go                    |   6 ++
 agentsession/env.go                      |  53 ++++++++++++++
 agentsession/runner.go                   |  10 ++-
 controllers/agent.go                     |   2 +-
 controllers/agent_provider.go            |  76 +------------------
 main.go                                  |   3 +
 service/agent_endpoint.go                | 121 +++++++++++++++++++++++++++++++
 web/src/components/ProviderAuditCard.tsx |  52 +++++++++----
 web/src/locales/en/data.json             |  17 +++--
 web/src/locales/zh/data.json             |  17 +++--
 13 files changed, 287 insertions(+), 105 deletions(-)

diff --git a/agentprovider/claude_code.go b/agentprovider/claude_code.go
index 635d278..b20922c 100644
--- a/agentprovider/claude_code.go
+++ b/agentprovider/claude_code.go
@@ -178,6 +178,18 @@ func (w claudeCodeWriter) envOf(target Target) 
(map[string]any, error) {
        return objectAt(config, "env"), nil
 }
 
+// sessionEnv points Claude Code at the endpoint for one run. It is what a
+// switch writes into settings.json, and beats it.
+func (w claudeCodeWriter) sessionEnv(endpoint Endpoint) map[string]string {
+       env := map[string]string{}
+       for key, value := range w.env(endpoint, false) {
+               if text, ok := value.(string); ok {
+                       env[key] = text
+               }
+       }
+       return env
+}
+
 // env is the block written into settings.json. Claude Code authenticates with
 // ANTHROPIC_AUTH_TOKEN, so the provider key goes there and not into
 // ANTHROPIC_API_KEY, which stays cleared.
diff --git a/agentprovider/gemini.go b/agentprovider/gemini.go
index d095e08..29ae8a4 100644
--- a/agentprovider/gemini.go
+++ b/agentprovider/gemini.go
@@ -215,6 +215,12 @@ func (w geminiWriter) Builtin(target Target, previous 
map[string]string) string
        return emptyAs(stringAt(objectAt(settings, "model"), "name"), 
geminiBuiltin)
 }
 
+// sessionEnv points the Gemini CLI at the endpoint for one run. Its .env file
+// never replaces a variable already exported, so this beats it.
+func (w geminiWriter) sessionEnv(endpoint Endpoint) map[string]string {
+       return w.env(endpoint, false)
+}
+
 // env is what a switch writes into ~/.gemini/.env. The base URL is what 
selects
 // the endpoint; the key is only there because the CLI insists on one.
 func (geminiWriter) env(endpoint Endpoint, masked bool) map[string]string {
diff --git a/agentprovider/provider.go b/agentprovider/provider.go
index 06be713..a01d33a 100644
--- a/agentprovider/provider.go
+++ b/agentprovider/provider.go
@@ -125,6 +125,23 @@ type writer interface {
        Builtin(Target, map[string]string) string
 }
 
+// sessionEnvWriter is a writer whose agent can be pointed at an endpoint by 
the
+// environment of one process, leaving its configuration file alone.
+type sessionEnvWriter interface {
+       sessionEnv(Endpoint) map[string]string
+}
+
+// SessionEnv is the environment that points one agent at endpoint for a single
+// run, empty for an agent that only reads its provider from a file - that file
+// is the one a switch already wrote.
+func SessionEnv(agentId string, endpoint Endpoint) map[string]string {
+       value, ok := writers[agentId].(sessionEnvWriter)
+       if !ok {
+               return nil
+       }
+       return value.sessionEnv(endpoint)
+}
+
 var (
        writers     = map[string]writer{}
        writerMutex sync.Mutex
diff --git a/agentprovider/qwen.go b/agentprovider/qwen.go
index 78d8871..384a019 100644
--- a/agentprovider/qwen.go
+++ b/agentprovider/qwen.go
@@ -217,6 +217,12 @@ func (w qwenWriter) Builtin(target Target, previous 
map[string]string) string {
        return emptyAs(stringAt(objectAt(settings, "model"), "name"), 
qwenBuiltin)
 }
 
+// sessionEnv points Qwen Code at the endpoint for one run. Its .env file never
+// replaces a variable already exported, so this beats it.
+func (w qwenWriter) sessionEnv(endpoint Endpoint) map[string]string {
+       return w.env(endpoint, false)
+}
+
 // env is what a switch writes into ~/.qwen/.env. The base URL selects the
 // endpoint; the key is only there because the CLI insists on one.
 func (qwenWriter) env(endpoint Endpoint, masked bool) map[string]string {
diff --git a/agentsession/env.go b/agentsession/env.go
new file mode 100644
index 0000000..ca8986c
--- /dev/null
+++ b/agentsession/env.go
@@ -0,0 +1,53 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package agentsession
+
+import (
+       "os"
+       "sync"
+)
+
+// EnvSource is what one driven agent is started with on top of Gateway's own
+// environment: the provider bound to it, for that one process. It lives 
outside
+// this package for the same reason the store does - driving an agent must not
+// depend on the database.
+type EnvSource func(Session) []string
+
+var envSource struct {
+       sync.RWMutex
+       source EnvSource
+}
+
+// SetEnvSource installs where a driven agent's provider is read from.
+func SetEnvSource(source EnvSource) {
+       envSource.Lock()
+       envSource.source = source
+       envSource.Unlock()
+}
+
+// environ is the environment one turn runs in. What the source adds is 
appended
+// rather than merged: os/exec keeps the last of a repeated variable, so a
+// provider Gateway resolved wins over one this process happens to carry.
+func environ(session Session) []string {
+       envSource.RLock()
+       source := envSource.source
+       envSource.RUnlock()
+
+       env := os.Environ()
+       if source == nil {
+               return env
+       }
+       return append(env, source(session)...)
+}
diff --git a/agentsession/runner.go b/agentsession/runner.go
index 9eb4ea2..a6cff5e 100644
--- a/agentsession/runner.go
+++ b/agentsession/runner.go
@@ -67,7 +67,7 @@ func run(ctx context.Context, headless *agent.Headless, 
session Session, prompt
        args := buildArgs(headless, session, prompt)
        cmd := newCommand(ctx, launch.Executable, args)
        cmd.Dir = workDir
-       cmd.Env = os.Environ()
+       cmd.Env = environ(session)
 
        stdout, err := cmd.StdoutPipe()
        if err != nil {
@@ -116,13 +116,19 @@ func run(ctx context.Context, headless *agent.Headless, 
session Session, prompt
                }
                return errors.New("stopped")
        }
+       // An agent that reported why it stopped is quoted, and the exit code it
+       // stopped with is not: "exit status 1" beside "Not logged in" says 
nothing
+       // the agent has not already said better.
+       if parseErr != nil {
+               return parseErr
+       }
        if waitErr != nil {
                if detail := strings.TrimSpace(stderr.String()); detail != "" {
                        return fmt.Errorf("%s: %s", waitErr, detail)
                }
                return waitErr
        }
-       return parseErr
+       return nil
 }
 
 // buildArgs writes the command line for one turn. A session the agent can 
carry
diff --git a/controllers/agent.go b/controllers/agent.go
index 2b9cf93..a688951 100644
--- a/controllers/agent.go
+++ b/controllers/agent.go
@@ -117,7 +117,7 @@ func (c *ApiController) GetAgents() {
                if !ok {
                        // A host with no address of its own cannot serve a 
sandboxed agent
                        // at all; the binding itself reports that, and the 
listing stays.
-                       baseUrl, _ = gatewayAgentUrl(installation.AgentId)
+                       baseUrl, _ = 
service.GatewayAgentUrl(installation.AgentId)
                        baseUrls[installation.AgentId] = baseUrl
                }
                item.ProxyBaseUrl = baseUrl
diff --git a/controllers/agent_provider.go b/controllers/agent_provider.go
index bbf4d70..f2e6705 100644
--- a/controllers/agent_provider.go
+++ b/controllers/agent_provider.go
@@ -16,14 +16,11 @@ package controllers
 
 import (
        "fmt"
-       "net"
-       "strconv"
        "strings"
 
        "github.com/apache/casbin-gateway/agent"
        "github.com/apache/casbin-gateway/agentpatch"
        "github.com/apache/casbin-gateway/agentprovider"
-       "github.com/apache/casbin-gateway/conf"
        "github.com/apache/casbin-gateway/object"
        "github.com/apache/casbin-gateway/service"
 )
@@ -38,7 +35,7 @@ func (c *ApiController) PlanAgentProvider() {
        if !ok {
                return
        }
-       endpoint, err := agentEndpoint(target.AgentId)
+       endpoint, err := service.AgentEndpoint(target.AgentId)
        if err != nil {
                c.ResponseError(err.Error())
                return
@@ -63,7 +60,7 @@ func (c *ApiController) ApplyAgentProvider() {
        if !ok {
                return
        }
-       endpoint, err := agentEndpoint(target.AgentId)
+       endpoint, err := service.AgentEndpoint(target.AgentId)
        if err != nil {
                c.ResponseError(err.Error())
                return
@@ -137,73 +134,6 @@ func checkAgentProtocol(agentId string, mode string, 
providerIds []string) error
        return nil
 }
 
-// agentEndpoint resolves where one agent should be pointed. In gateway mode
-// that is the local proxy, which is what makes a later switch take effect
-// without rewriting a file; in direct mode it is the provider's own upstream.
-func agentEndpoint(agentId string) (agentprovider.Endpoint, error) {
-       endpoint := agentprovider.Endpoint{Mode: object.ModeGateway}
-
-       stored, err := object.GetAgent(agentId)
-       if err != nil {
-               return endpoint, err
-       }
-       if stored != nil && stored.Mode != "" {
-               endpoint.Mode = stored.Mode
-       }
-
-       provider, err := object.GetProviderByAgent(agentId)
-       if err != nil {
-               return endpoint, err
-       }
-
-       endpoint.Provider = provider.GetId()
-       endpoint.Protocol = object.ProviderApiFamily(provider)
-       endpoint.ClientAuth = object.UsesClientAuth(provider)
-       if len(provider.Models) > 0 {
-               endpoint.Model = provider.Models[0]
-               endpoint.Models = provider.Models
-       }
-
-       if endpoint.Mode == object.ModeDirect {
-               endpoint.BaseUrl = provider.BaseUrl
-               endpoint.ApiKey = provider.ApiKey
-               endpoint.ServesResponsesApi = 
object.ServesResponsesApi(provider)
-               return endpoint, nil
-       }
-
-       baseUrl, err := gatewayAgentUrl(agentId)
-       if err != nil {
-               return endpoint, err
-       }
-       endpoint.BaseUrl = baseUrl
-       endpoint.ServesResponsesApi = true
-       // A client-auth provider forwards whatever the agent sends, so it must 
keep
-       // sending its own credentials: a placeholder token written into the 
agent's
-       // configuration would replace the sign-in it already has.
-       if !endpoint.ClientAuth {
-               endpoint.ApiKey = conf.GetRelayToken()
-       }
-       return endpoint, nil
-}
-
-// gatewayAgentUrl is the base URL an agent reaches its own provider at. One 
URL
-// serves every wire format: an OpenAI client appends /chat/completions to it,
-// Codex appends /responses, and an Anthropic one appends /v1/messages. An 
agent
-// that runs its sessions in a sandbox is named this host's own address instead
-// of loopback, which inside a sandbox is the sandbox itself.
-func gatewayAgentUrl(agentId string) (string, error) {
-       host := "127.0.0.1"
-       if agent.RunsSandboxed(agentId) {
-               lan, err := service.LanHost()
-               if err != nil {
-                       return "", err
-               }
-               host = lan
-       }
-       address := net.JoinHostPort(host, strconv.Itoa(conf.GetHttpPort()))
-       return fmt.Sprintf("http://%s/v1/agents/%s";, address, agentId), nil
-}
-
 // reapplyAgentProvider writes the bound provider into the configuration of 
every
 // installation of one agent, which is what makes a routing change reach the
 // agent at all: an installation Gateway never wrote keeps talking to the
@@ -216,7 +146,7 @@ func reapplyAgentProvider(agentId string) string {
                return err.Error()
        }
 
-       endpoint, endpointErr := agentEndpoint(agentId)
+       endpoint, endpointErr := service.AgentEndpoint(agentId)
        failures := []string{}
        for _, installation := range installations {
                if installation.AgentId != agentId {
diff --git a/main.go b/main.go
index c9460d9..e8827af 100644
--- a/main.go
+++ b/main.go
@@ -128,6 +128,9 @@ func main() {
        // The conversations Gateway drives on somebody's behalf, put back 
where a
        // restart left them.
        object.InitAgentSessions()
+       // A driven agent answers through the provider bound to it, so a session
+       // needs no sign-in of the agent's own.
+       service.InitAgentSessionEnv()
 
        // Monitoring is on by default, so the agents already on this host are
        // patched without anyone opening the UI. The scan walks the disk, 
hence the
diff --git a/service/agent_endpoint.go b/service/agent_endpoint.go
new file mode 100644
index 0000000..86cac74
--- /dev/null
+++ b/service/agent_endpoint.go
@@ -0,0 +1,121 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+       "fmt"
+       "net"
+       "strconv"
+
+       "github.com/apache/casbin-gateway/agent"
+       "github.com/apache/casbin-gateway/agentprovider"
+       "github.com/apache/casbin-gateway/agentsession"
+       "github.com/apache/casbin-gateway/conf"
+       "github.com/apache/casbin-gateway/object"
+)
+
+// AgentEndpoint resolves where one agent should be pointed. In gateway mode
+// that is the local proxy, which is what makes a later switch take effect
+// without rewriting a file; in direct mode it is the provider's own upstream.
+func AgentEndpoint(agentId string) (agentprovider.Endpoint, error) {
+       endpoint := agentprovider.Endpoint{Mode: object.ModeGateway}
+
+       stored, err := object.GetAgent(agentId)
+       if err != nil {
+               return endpoint, err
+       }
+       if stored != nil && stored.Mode != "" {
+               endpoint.Mode = stored.Mode
+       }
+
+       provider, err := object.GetProviderByAgent(agentId)
+       if err != nil {
+               return endpoint, err
+       }
+
+       endpoint.Provider = provider.GetId()
+       endpoint.Protocol = object.ProviderApiFamily(provider)
+       endpoint.ClientAuth = object.UsesClientAuth(provider)
+       if len(provider.Models) > 0 {
+               endpoint.Model = provider.Models[0]
+               endpoint.Models = provider.Models
+       }
+
+       if endpoint.Mode == object.ModeDirect {
+               endpoint.BaseUrl = provider.BaseUrl
+               endpoint.ApiKey = provider.ApiKey
+               endpoint.ServesResponsesApi = 
object.ServesResponsesApi(provider)
+               return endpoint, nil
+       }
+
+       baseUrl, err := GatewayAgentUrl(agentId)
+       if err != nil {
+               return endpoint, err
+       }
+       endpoint.BaseUrl = baseUrl
+       endpoint.ServesResponsesApi = true
+       // A client-auth provider forwards whatever the agent sends, so it must 
keep
+       // sending its own credentials: a placeholder token written into the 
agent's
+       // configuration would replace the sign-in it already has.
+       if !endpoint.ClientAuth {
+               endpoint.ApiKey = conf.GetRelayToken()
+       }
+       return endpoint, nil
+}
+
+// GatewayAgentUrl is the base URL an agent reaches its own provider at. One 
URL
+// serves every wire format: an OpenAI client appends /chat/completions to it,
+// Codex appends /responses, and an Anthropic one appends /v1/messages. An 
agent
+// that runs its sessions in a sandbox is named this host's own address instead
+// of loopback, which inside a sandbox is the sandbox itself.
+func GatewayAgentUrl(agentId string) (string, error) {
+       host := "127.0.0.1"
+       if agent.RunsSandboxed(agentId) {
+               lan, err := LanHost()
+               if err != nil {
+                       return "", err
+               }
+               host = lan
+       }
+       address := net.JoinHostPort(host, strconv.Itoa(conf.GetHttpPort()))
+       return fmt.Sprintf("http://%s/v1/agents/%s";, address, agentId), nil
+}
+
+// InitAgentSessionEnv gives the session driver the provider bound to each 
agent.
+func InitAgentSessionEnv() {
+       agentsession.SetEnvSource(agentSessionEnv)
+}
+
+// agentSessionEnv is the environment one driven agent is started with. Gateway
+// hands the session the endpoint it would have written into the agent's
+// configuration, for that one process: a driven agent then answers through the
+// provider bound to it whether or not the switch was ever applied, and without
+// a sign-in of its own.
+//
+// Nothing is handed over for an agent with no provider bound, or one whose
+// endpoint only lives in a file. Those keep reading their own configuration,
+// which is what they do when somebody types at them.
+func agentSessionEnv(session agentsession.Session) []string {
+       endpoint, err := AgentEndpoint(session.AgentId)
+       if err != nil || endpoint.BaseUrl == "" {
+               return nil
+       }
+
+       env := []string{}
+       for key, value := range agentprovider.SessionEnv(session.AgentId, 
endpoint) {
+               env = append(env, key+"="+value)
+       }
+       return env
+}
diff --git a/web/src/components/ProviderAuditCard.tsx 
b/web/src/components/ProviderAuditCard.tsx
index c6e3d8c..1de7596 100644
--- a/web/src/components/ProviderAuditCard.tsx
+++ b/web/src/components/ProviderAuditCard.tsx
@@ -91,6 +91,7 @@ function CheckTile({
   caseName,
   value,
   detail,
+  quote,
   level,
   weight,
   footer,
@@ -102,6 +103,9 @@ function CheckTile({
   caseName?: string;
   value: React.ReactNode;
   detail: React.ReactNode;
+  /** The upstream's own words, kept apart from the sentence Gateway wrote
+   * about them: one of the two is evidence and the other is a reading of it. 
*/
+  quote?: string;
   level: LlmAuditLevel;
   /** What this case was worth in the score, where it counted towards one. */
   weight?: number;
@@ -137,6 +141,11 @@ function CheckTile({
       ) : null}
       <span className={cn("text-lg font-semibold tabular-nums", 
style.value)}>{value}</span>
       <span className="text-muted-foreground text-xs 
leading-snug">{detail}</span>
+      {quote ? (
+        <p className="text-foreground/75 line-clamp-3 border-l-2 pl-2 
font-mono text-[11px] leading-snug break-words whitespace-pre-wrap">
+          {quote}
+        </p>
+      ) : null}
       {footer}
     </div>
   );
@@ -205,6 +214,9 @@ function ProbeEvidence({
             <EvidenceBlock label={i18next.t("audit:What came back")} 
value={check.got} />
             <EvidenceBlock label={i18next.t("audit:What a pass looks like")} 
value={check.want} />
             <EvidenceBlock label={i18next.t("audit:What was sent")} 
value={check.sent} />
+            <p className="text-muted-foreground text-xs leading-relaxed">
+              {i18next.t("audit:Judged here")}
+            </p>
           </div>
         </DialogContent>
       </Dialog>
@@ -306,9 +318,9 @@ function probeValue(check: ProbeCheck) {
  * is the part that can be sent to the provider and argued about.
  */
 /**
- * The cases that read the answer rather than the envelope. Each of them 
carries
- * what came back as its first fact and the answer itself as the second, so the
- * tile can quote the sentence the level was drawn from.
+ * The cases that read the answer rather than the envelope. The sentence says
+ * what was made of the answer; the answer itself is quoted under it, so nobody
+ * has to take the reading on trust.
  */
 function probeAnswerDetail(check: ProbeCheck) {
   const [outcome, answer, extra, wanted] = check.facts;
@@ -322,27 +334,25 @@ function probeAnswerDetail(check: ProbeCheck) {
   switch (check.key) {
   case "knowledge":
     if (outcome === "missed") {
-      return fill("audit:Answer wrong", {answer: answer ?? "", expected: extra 
?? ""});
+      return fill("audit:Answer wrong", {expected: extra ?? ""});
     }
     if (outcome === "forbidden") {
-      return fill("audit:Answer forbidden", {answer: answer ?? "", forbidden: 
extra ?? ""});
+      return fill("audit:Answer forbidden", {forbidden: extra ?? ""});
     }
-    return fill("audit:Answer right", {answer: answer ?? ""});
+    return i18next.t("audit:Answer right");
   case "selfid":
     if (outcome === "undocumented") {
       return i18next.t("audit:Self undocumented");
     }
     if (outcome === "other") {
-      return fill("audit:Self other", {answer: answer ?? "", other: extra ?? 
"", vendor: wanted ?? ""});
+      return fill("audit:Self other", {other: extra ?? "", vendor: wanted ?? 
""});
     }
     if (outcome === "silent") {
-      return fill("audit:Self silent", {answer: answer ?? "", vendor: extra ?? 
""});
+      return fill("audit:Self silent", {vendor: extra ?? ""});
     }
-    return fill("audit:Self match", {answer: answer ?? "", vendor: extra ?? 
""});
+    return fill("audit:Self match", {vendor: extra ?? ""});
   case "hidden":
-    return outcome === "hidden"
-      ? fill("audit:Hidden found", {answer: answer ?? ""})
-      : i18next.t("audit:Hidden none");
+    return outcome === "hidden" ? i18next.t("audit:Hidden found") : 
i18next.t("audit:Hidden none");
   case "feature":
     if (outcome === "rejected") {
       return fill("audit:Parameter rejected", {reason: answer ?? ""});
@@ -365,7 +375,7 @@ function probeAnswerDetail(check: ProbeCheck) {
       return fill("audit:Repeat tokens", {counts: check.facts.slice(1).join(", 
")});
     }
     if (outcome === "answers") {
-      return fill("audit:Repeat answers", {first: answer ?? "", second: extra 
?? ""});
+      return i18next.t("audit:Repeat answers");
     }
     return fill("audit:Repeat same", {count: answer ?? ""});
   }
@@ -373,6 +383,21 @@ function probeAnswerDetail(check: ProbeCheck) {
 
 const answerKeys: ProbeCheck["key"][] = ["knowledge", "selfid", "hidden", 
"feature", "repeat"];
 
+/** The cases whose answer is writing, which is what a tile can quote as it 
is. */
+const quotedKeys: ProbeCheck["key"][] = ["knowledge", "selfid", "hidden", 
"repeat"];
+
+/**
+ * The upstream's own words, for the tile to show under the finding. A question
+ * that never got asked has none to show: the sentence already carries the
+ * refusal, and there is nothing the model wrote to quote.
+ */
+function probeQuote(check: ProbeCheck) {
+  if (!quotedKeys.includes(check.key) || !check.got) {
+    return "";
+  }
+  return ["failed", "empty", "undocumented"].includes(check.facts[0] ?? "") ? 
"" : check.got;
+}
+
 function probeDetail(check: ProbeCheck, probe: ProviderProbe) {
   if (answerKeys.includes(check.key)) {
     return probeAnswerDetail(check);
@@ -552,6 +577,7 @@ function ProbeSection({
                   caseName={check.case}
                   value={probeValue(check)}
                   detail={detail}
+                  quote={probeQuote(check)}
                   level={check.level}
                   weight={check.weight}
                   footer={
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index cd33393..7648423 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -614,11 +614,11 @@
     "Allowed drift": "Allowed drift",
     "Allowed drift hint": "How far two byte-identical requests may be billed 
apart before it counts as invented.",
     "Answer empty": "Nothing was said, which is the answer length or the 
thinking rather than the answer.",
-    "Answer forbidden": "The answer carries \"{forbidden}\", which this case 
rules out: {answer}",
+    "Answer forbidden": "The answer carries \"{forbidden}\", which this case 
rules out.",
     "Answer length": "Answer length",
     "Answer length hint": "The max_tokens the request asks for. The envelope 
is what is measured, not the writing.",
-    "Answer right": "It answered: {answer}",
-    "Answer wrong": "It answered \"{answer}\", where the answer is 
{expected}.",
+    "Answer right": "It gave an answer this case accepts.",
+    "Answer wrong": "The answer is {expected}.",
     "Authenticity": "Authenticity",
     "Authenticity description": "Whether the API behind each key is what it 
was sold as. Every provider is probed on its own, without being asked, and 
graded on a published suite of test cases: what each case asks, how it decides 
and what it is worth are all below, and all of them can be changed.",
     "Billing detail": "{billed} input tokens billed for about {estimate} sent. 
Four characters to the token is rough, so only a wide gap means anything.",
@@ -687,7 +687,7 @@
     "Headers looked for": "Headers looked for",
     "Headers required": "Headers required",
     "Held": "Held",
-    "Hidden found": "The request carried no system prompt, and something else 
came back: {answer}",
+    "Hidden found": "The request carried no system prompt, and something else 
came back.",
     "Hidden instructions": "Hidden instructions",
     "Hidden none": "Nothing stands above this request but what this gateway 
sent.",
     "Honored": "Honored",
@@ -698,6 +698,7 @@
     "Identity ok": "Answered as {answered}. Echoing a name back is easy, so 
this is a floor rather than a proof.",
     "Identity reads": "The model name on the first answer of the run.",
     "Identity unverified": "The name that came back is the name that was sent 
({answered}), and no vendor documents a model by that name. An echo is not 
something that can be checked.",
+    "Judged here": "Nothing above was decided by a model. Gateway compared 
what came back with the line beside it - the answers the case accepts, or the 
field this API documents - here on this machine.",
     "Last seen": "Last seen",
     "Latency detail": "Slowest 5% from {p95}, {ratio}x the median.",
     "Lost": "Lost",
@@ -759,7 +760,7 @@
     "Rejected answers": "Rejected answers",
     "Rejected answers hint": "An answer carrying any of these fails the case.",
     "Removed": "No longer configured",
-    "Repeat answers": "Identical requests were answered differently: 
\"{first}\" and \"{second}\". Sampling is allowed to differ, so this is a 
note.",
+    "Repeat answers": "Identical requests were answered differently. Sampling 
is allowed to differ, so this is a note.",
     "Repeat models": "Identical requests came back with different model names: 
{names}",
     "Repeat same": "{count} identical requests came back with the same model 
name and the same input count.",
     "Repeat tokens": "The same bytes were counted as different amounts of 
input: {counts}. A different tokenizer is a different model.",
@@ -781,9 +782,9 @@
     "See how this is judged": "See what this case asks and how the answer is 
judged",
     "See the evidence": "See the evidence",
     "See what came back": "See what came back",
-    "Self match": "It says it is {vendor}'s: {answer}",
-    "Self other": "Sold as a model of {vendor}, it says it is {other}'s: 
{answer}",
-    "Self silent": "It never names {vendor}, and never names anyone else 
either: {answer}",
+    "Self match": "It says it is {vendor}'s.",
+    "Self other": "Sold as a model of {vendor}, it says it is {other}'s.",
+    "Self silent": "It never names {vendor}, and never names anyone else 
either.",
     "Self undocumented": "The name this was sold under belongs to no vendor's 
catalogue, so there is nothing its answer would have to agree with.",
     "Self-reported maker": "Self-reported maker",
     "Stream complete": "Every event this API documents arrived, in order.",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 6654552..70d1242 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -614,11 +614,11 @@
     "Allowed drift": "允许的漂移",
     "Allowed drift hint": "两个逐字节相同的请求,计费相差多少以内还算正常,超过就是编出来的。",
     "Answer empty": "什么都没答出来,多半是回答长度不够或者全被思考占满了,不算答错。",
-    "Answer forbidden": "回答里出现了这道题明确排除的「{forbidden}」:{answer}",
+    "Answer forbidden": "回答里出现了这条用例明确排除的“{forbidden}”。",
     "Answer length": "回答长度",
     "Answer length hint": "请求里的max_tokens。要测的是信封,不是文笔。",
-    "Answer right": "回答:{answer}",
-    "Answer wrong": "回答是「{answer}」,而这道题的答案是{expected}。",
+    "Answer right": "回答落在这条用例接受的答案里。",
+    "Answer wrong": "正确答案是{expected}。",
     "Authenticity": "真伪检测",
     "Authenticity description": "每个API 
Key背后的服务是否就是它所声称的那个。每个供应商都会被自动探测,无需手动触发,并按一套公开的测试用例打分:每条用例问什么、怎么判定、占多少权重都列在下面,而且都可以修改。",
     "Billing detail": "实际发出约{estimate} 
token,被计了{billed}。四字符折一token只是粗估,差得多才说明问题。",
@@ -687,7 +687,7 @@
     "Headers looked for": "查找的响应头",
     "Headers required": "至少要有几个",
     "Held": "接住了",
-    "Hidden found": "请求里并没有系统提示,它却答出了别的内容:{answer}",
+    "Hidden found": "请求里并没有系统提示,回来的却不止本次的内容。",
     "Hidden instructions": "隐藏指令",
     "Hidden none": "这次请求上面没有本网关之外的任何东西。",
     "Honored": "生效",
@@ -698,6 +698,7 @@
     "Identity ok": "返回{answered}。把名字原样回显是最容易做到的事,所以这只是底线,不是证明。",
     "Identity reads": "本次探测第一个回答里的模型名。",
     "Identity unverified": 
"回来的模型名就是发出去的那个({answered}),而这个名字不在任何已知厂商的型号表里——回声而已,证明不了什么。",
+    "Judged here": 
"上面这些结论没有经过任何模型。Gateway只是在本机把返回和旁边那行做了比对——用例接受的答案,或者这套API文档规定的字段。",
     "Last seen": "最后一次",
     "Latency detail": "最慢的5%从{p95}起,是中位数的{ratio}倍。",
     "Lost": "没接住",
@@ -759,7 +760,7 @@
     "Rejected answers": "算答错的答案",
     "Rejected answers hint": "回答里只要出现其中任意一条,这道题就算没过。",
     "Removed": "已删除",
-    "Repeat answers": "同样的请求答出了不同的内容:「{first}」和「{second}」。采样本来就允许不同,仅作参考。",
+    "Repeat answers": "同样的请求回来了不同的内容。采样本来就允许不同,所以这只是个提示。",
     "Repeat models": "同样的请求,回来的模型名却不一样:{names}",
     "Repeat same": "{count}次完全相同的请求,回来的模型名和输入token数都一样。",
     "Repeat tokens": "同样的字节,被算成了不同的输入token数:{counts}。分词器不同,模型就不是同一个。",
@@ -781,9 +782,9 @@
     "See how this is judged": "看看这道题问的是什么、怎么判对错",
     "See the evidence": "查看证据",
     "See what came back": "查看实际返回",
-    "Self match": "它说自己出自{vendor}:{answer}",
-    "Self other": "卖的是{vendor}的模型,它却说自己出自{other}:{answer}",
-    "Self silent": "它没说自己出自{vendor},也没说是别家:{answer}",
+    "Self match": "它说自己出自{vendor}。",
+    "Self other": "卖的是{vendor}的模型,它却说自己出自{other}。",
+    "Self silent": "它没说自己出自{vendor},也没说是别家。",
     "Self undocumented": "这个模型名不属于任何一家已知厂商的型号表,它的回答也就没有可对照的对象。",
     "Self-reported maker": "自述来历",
     "Stream complete": "该API文档里的事件全部按顺序到齐。",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to