Files
oh-my-pi/packages/ai/test/github-copilot-error.test.ts
T
Dimitar Ganev af50d3bfa4 fix(ai): retry GitHub Copilot transient model_not_supported 400s
GitHub Copilot intermittently returns `HTTP 400 model_not_supported`
for preview models (gpt-5.3-codex, gpt-5.4, gpt-5.4-mini, ...) on OAuth
clients other than VS Code, even when `/models` reports the model as
enabled. Root cause is a per-OAuth-client rollout gap across Copilot's
responses backend; repeating the identical request typically lands on
a backend that has the model. See opencode#13313.

- Add `isCopilotTransientModelError` and `callWithCopilotModelRetry`
  in `utils/retry` (3 attempts, linear backoff, abort-aware, no-op
  for non-Copilot providers).
- Wrap `client.responses.create` in `openai-responses` and the
  initial completions stream in `openai-completions` with the retry.
- Extend Anthropic `isProviderRetryableError` to treat Copilot
  transient model errors as provider-retryable.
- Rename `rewriteCopilotAuthError` to `rewriteCopilotError` and add
  a 400 `model_not_supported` rewrite surfacing actionable guidance
  (retry, switch to a GA model, or run from VS Code) after retries
  exhaust.
- Rename test file accordingly and add a dedicated retry unit test.
2026-04-18 22:32:51 +02:00

60 lines
2.6 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { rewriteCopilotError } from "../src/utils/http-inspector";
function errorWithStatus(status: number): Error {
const err = new Error(`${status} Unauthorized`);
(err as any).status = status;
return err;
}
describe("rewriteCopilotError", () => {
it("returns original message for non-copilot providers", () => {
const err = errorWithStatus(401);
expect(rewriteCopilotError("some error", err, "openai")).toBe("some error");
});
it("returns original message for non-401/403 errors", () => {
const err = errorWithStatus(500);
expect(rewriteCopilotError("server error", err, "github-copilot")).toBe("server error");
});
it("rewrites message for 401 with github-copilot provider", () => {
const err = errorWithStatus(401);
const result = rewriteCopilotError("401 Unauthorized: ...", err, "github-copilot");
expect(result).toContain("GitHub Copilot authentication failed (HTTP 401)");
expect(result).toContain("/login github-copilot");
});
it("rewrites 403 with access-denied message (not auth-failed, to avoid credential removal)", () => {
const err = errorWithStatus(403);
const result = rewriteCopilotError("403 Forbidden", err, "github-copilot");
expect(result).toContain("GitHub Copilot access denied (HTTP 403)");
expect(result).not.toContain("GitHub Copilot authentication failed");
expect(result).not.toContain("/login github-copilot");
});
it("rewrites 400 model_not_supported with rollout-gap guidance", () => {
const err = new Error("400 The requested model is not supported.");
(err as unknown as { status: number; code: string }).status = 400;
(err as unknown as { status: number; code: string }).code = "model_not_supported";
const result = rewriteCopilotError("original", err, "github-copilot");
expect(result).toContain("HTTP 400 model_not_supported");
expect(result).toContain("rollout gap");
expect(result).not.toContain("authentication failed");
});
it("leaves non-copilot 400 model_not_supported untouched", () => {
const err = new Error("400 model_not_supported");
(err as unknown as { status: number; code: string }).status = 400;
(err as unknown as { status: number; code: string }).code = "model_not_supported";
expect(rewriteCopilotError("orig", err, "openai")).toBe("orig");
});
it("leaves 400 without model_not_supported code untouched", () => {
const err = new Error("400 invalid request");
(err as unknown as { status: number; code: string }).status = 400;
(err as unknown as { status: number; code: string }).code = "invalid_request_body";
expect(rewriteCopilotError("orig", err, "github-copilot")).toBe("orig");
});
});