Raw API failures make models hallucinate. GitHub answers 404 for private repositories the token cannot see, and an expired Vercel OIDC token surfaces as a bare "Not authorized" — both read as "the repo is private" or "permissions are missing" to a model, which then confidently reports the wrong cause.
The SDK maps every failure it can classify to a structured error from an evlog catalog. Each error carries:
code— stable, machine-readable identifier (github_tools.NOT_FOUND)message— what happened, with the GitHub or Connect detail embeddedwhy— the technical cause, including the non-obvious onesfix— the actionable remedylink— the relevant documentation page (this site for auth/Connect setup, GitHub's REST troubleshooting docs for API errors)
Error codes
Authentication and Vercel Connect
| Code | When |
|---|---|
TOKEN_REQUIRED | No token string, provider, or GITHUB_TOKEN env var was available |
OIDC_TOKEN_EXPIRED | VERCEL_OIDC_TOKEN is past its exp claim — thrown before any request is made, with the exact expiry time. Run vercel env pull locally |
CONNECT_NOT_AUTHORIZED | Connect rejected the calling process's identity (403). The request never reached GitHub — this is not a GitHub permission problem |
CONNECT_USER_NOT_CONNECTED | A { type: 'user' } subject was requested but that user has no active GitHub connection |
CONNECT_INSTALLATION_REQUIRED | The connector's GitHub App is not installed on the target account |
SUBJECT_CONTEXT_REQUIRED | A connect.subject resolver ran outside a tool execution (no eve context) |
GitHub API
| Code | Status | When |
|---|---|---|
UNAUTHORIZED | 401 | Token invalid, expired, or revoked |
FORBIDDEN | 403 | Missing scope, SAML enforcement, or a user-token-only API (gists, notifications) called with an installation token |
RATE_LIMITED | 403/429 | Rate limit exhausted — the message includes remaining/limit and the reset timestamp |
NOT_FOUND | 404 | The resource does not exist or the token cannot see it — GitHub returns 404 instead of 403 for private resources |
VALIDATION_FAILED | 422 | Well-formed input that GitHub refused, with GitHub's message embedded |
Statuses without a catalog entry (5xx, redirects) pass through unchanged. The original Octokit RequestError stays reachable as cause; request coordinates live in internal and are never serialized toward the model.
What the model sees
In the eve extension, a failing tool returns the structure instead of a flat string:
{
"error": {
"code": "github_tools.NOT_FOUND",
"message": "GitHub resource not found (404): Not Found",
"why": "Either the resource does not exist, or the token cannot see it — GitHub deliberately returns 404 instead of 403 for private resources the token has no access to.",
"fix": "Check the owner/repo/number input first. If it is correct, the token lacks access: grant the repository to the PAT or App installation, or use a Connect subject that has access.",
"link": "https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api#404-not-found-for-an-existing-resource"
}
}
internal stays off the wire (request URL, raw status). link is included so the model can fetch the docs page when why/fix are not enough.
AI SDK consumers
With generateText/streamText, tool execution errors are thrown and the framework forwards error.message to the model. The catalog messages are written to be self-sufficient for that path. If you need the full structure (error boundaries, logging, custom onError), use evlog's parseError:
import { parseError } from 'evlog'
try {
await generateText({ model, tools, prompt })
}
catch (error) {
const { code, message, why, fix } = parseError(error)
// code autocompletes against the github_tools catalog
}
The catalog itself is exported as githubToolsErrors from @github-tools/sdk for consumers who want to match on specific factories or reuse the codes.