create employee table
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 9s

This commit is contained in:
2026-09-05 10:44:47 +05:30
parent dc785b917c
commit cf99866e12
11 changed files with 820 additions and 26 deletions

View File

@@ -345,15 +345,33 @@ func translate(err error) error {
return &Error{Code: CodeUpstream, Message: "the model call failed", Cause: err}
}
// The upstream reason, carried through when there is one — the same rule the
// OpenAI path already follows, and for the same reason it gives: a 400 that
// says which model id or tool schema was rejected is worth more than "the
// model rejected the request", and the trajectory records only the message.
//
// This was not a hypothetical. Every run on a deployment failed with a bare
// `gateway.invalid_request`, and neither the run detail nor anything a
// client could read said why — the one fact needed to fix it was discarded
// here, three lines from where it arrived. `Cause` keeps the full error for
// a Go caller; nothing reads it by the time a run is persisted.
detail := anthropicErrorMessage(apierr.RawJSON())
withDetail := func(base string) string {
if detail == "" {
return base
}
return base + ": " + detail
}
switch apierr.StatusCode {
case 400:
return &Error{Code: CodeInvalidRequest, Message: "the model rejected the request", Status: 400, Cause: err}
return &Error{Code: CodeInvalidRequest, Message: withDetail("the model rejected the request"), Status: 400, Cause: err}
case 401, 403:
return &Error{Code: CodeUnauthorized, Message: "the model credentials were refused", Status: apierr.StatusCode, Cause: err}
return &Error{Code: CodeUnauthorized, Message: withDetail("the model credentials were refused"), Status: apierr.StatusCode, Cause: err}
case 408:
return &Error{Code: CodeTimeout, Message: "the model call timed out", Status: 408, Cause: err}
return &Error{Code: CodeTimeout, Message: withDetail("the model call timed out"), Status: 408, Cause: err}
case 429:
return &Error{Code: CodeRateLimited, Message: "the model is rate limiting this deployment", Status: 429, Cause: err}
return &Error{Code: CodeRateLimited, Message: withDetail("the model is rate limiting this deployment"), Status: 429, Cause: err}
case 529:
// Anthropic's "overloaded" — the service is up and temporarily out of
// capacity. Named separately from the 500s because it is the one that
@@ -368,12 +386,36 @@ func translate(err error) error {
// message.
return &Error{
Code: CodeUpstream,
Message: fmt.Sprintf("the model call failed (http %d)", apierr.StatusCode),
Message: withDetail(fmt.Sprintf("the model call failed (http %d)", apierr.StatusCode)),
Status: apierr.StatusCode, Cause: err,
}
}
}
// anthropicErrorMessage digs the human-readable reason out of an error body.
//
// Best-effort, exactly like its OpenAI counterpart: the envelope is documented
// and stable enough to be worth reading, and an unparseable body yields nothing
// rather than failing a failure.
func anthropicErrorMessage(raw string) string {
if raw == "" {
return ""
}
var envelope struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
}
if err := json.Unmarshal([]byte(raw), &envelope); err != nil {
return ""
}
if envelope.Error.Message != "" {
return envelope.Error.Message
}
return envelope.Error.Type
}
/* ── Streaming ──────────────────────────────────────────────────────────── */
// Stream is Complete, with the assistant's text delivered as it arrives.

View File

@@ -0,0 +1,46 @@
package gateway
import "testing"
// The reason a 400 gives is the whole diagnosis, and it used to be thrown away.
//
// A deployment answered every single run with "The agent could not finish —
// something it needed did not answer." Retrieval worked, the run was created,
// the provider was reached, and the trajectory recorded `gateway.invalid_request`
// and nothing else. The actual cause was one sentence long and Anthropic had
// sent it: the account was out of credit. Nothing a client, a log or a run
// detail could show said so, because `translate` dropped the body.
//
// The envelope below is the real one, copied from that response.
func TestAnthropicErrorMessage(t *testing.T) {
cases := []struct {
name string
raw string
want string
}{
{
name: "the outage this test exists for",
raw: `{"type":"error","error":{"type":"invalid_request_error",` +
`"message":"Your credit balance is too low to access the Anthropic API. ` +
`Please go to Plans & Billing to upgrade or purchase credits."},` +
`"request_id":"req_011CeiJqr8rTdpbZYhcL6VHG"}`,
want: "Your credit balance is too low to access the Anthropic API. " +
"Please go to Plans & Billing to upgrade or purchase credits.",
},
{
name: "a message-less envelope falls back to the type",
raw: `{"type":"error","error":{"type":"overloaded_error"}}`,
want: "overloaded_error",
},
// Best-effort by design: a failure to read a failure must not become a
// second failure.
{name: "unparseable body yields nothing", raw: `not json at all`, want: ""},
{name: "empty body yields nothing", raw: ``, want: ""},
}
for _, c := range cases {
if got := anthropicErrorMessage(c.raw); got != c.want {
t.Errorf("%s: anthropicErrorMessage() = %q, want %q", c.name, got, c.want)
}
}
}