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]
