@gajae-code/ai 0.12.7 → 0.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/auth-storage.d.ts +33 -6
  3. package/dist/types/index.d.ts +2 -1
  4. package/dist/types/model-manager.d.ts +2 -0
  5. package/dist/types/model-pricing.d.ts +3 -0
  6. package/dist/types/provider-models/special.d.ts +1 -0
  7. package/dist/types/providers/composer-discipline.d.ts +29 -23
  8. package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
  9. package/dist/types/providers/openai-responses-shared.d.ts +1 -0
  10. package/dist/types/providers/register-builtins.d.ts +2 -2
  11. package/dist/types/types.d.ts +15 -7
  12. package/dist/types/utils/fallback-transport.d.ts +23 -0
  13. package/dist/types/utils/oauth/anthropic.d.ts +21 -2
  14. package/dist/types/utils/oauth/callback-server.d.ts +7 -0
  15. package/dist/types/utils/oauth/types.d.ts +12 -1
  16. package/package.json +2 -2
  17. package/src/auth-gateway/server.ts +6 -0
  18. package/src/auth-storage.ts +325 -39
  19. package/src/index.ts +2 -0
  20. package/src/model-manager.ts +9 -3
  21. package/src/model-pricing.ts +68 -0
  22. package/src/model-thinking.ts +23 -1
  23. package/src/models.json +122 -24
  24. package/src/models.ts +7 -4
  25. package/src/prompts/composer-bash-policy-recovery.md +1 -0
  26. package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
  27. package/src/prompts/cursor-composer-edit-discipline.md +7 -0
  28. package/src/provider-models/descriptors.ts +7 -1
  29. package/src/provider-models/special.ts +8 -0
  30. package/src/providers/composer-discipline.ts +54 -0
  31. package/src/providers/cursor.ts +2 -2
  32. package/src/providers/openai-codex/response-handler.ts +24 -2
  33. package/src/providers/openai-codex-responses.ts +5 -1
  34. package/src/providers/openai-completions.ts +109 -23
  35. package/src/providers/openai-opencodex-responses.ts +173 -0
  36. package/src/providers/openai-responses-shared.ts +14 -3
  37. package/src/providers/openai-responses.ts +91 -13
  38. package/src/providers/register-builtins.ts +4 -4
  39. package/src/stream.ts +61 -6
  40. package/src/types.ts +17 -6
  41. package/src/utils/discovery/openai-compatible.ts +18 -2
  42. package/src/utils/fallback-transport.ts +79 -6
  43. package/src/utils/http-inspector.ts +1 -0
  44. package/src/utils/idle-iterator.ts +2 -0
  45. package/src/utils/oauth/anthropic.ts +41 -8
  46. package/src/utils/oauth/callback-server.ts +64 -16
  47. package/src/utils/oauth/index.ts +5 -0
  48. package/src/utils/oauth/types.ts +13 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.10] - 2026-08-03
6
+
7
+ ## [0.12.9] - 2026-08-03
8
+ ### Added
9
+
10
+ - Anthropic OAuth can now pair by pasting the authorization code Anthropic displays (`https://platform.claude.com/oauth/code/callback`) instead of waiting on `http://localhost:54545/callback`, so a browser with no network route back to the machine running gjc can complete the login. Opt in per login with `OAuthLoginOptions.manualCode`; the loopback flow stays the default and is unchanged. Callback flows can now opt out of binding a local listener entirely (`OAuthCallbackFlowOptions.skipCallbackServer`), which fails fast when no manual code handler is supplied instead of idling until the five-minute timeout. The hosted redirect is a hard-coded constant with no env or config override, so it cannot be repointed at an attacker-controlled collector.
11
+
12
+ ### Fixed
13
+
14
+ - Composer shell-policy failures now expose a stable structured marker plus provider-specific recovery guidance, while retaining recognition of prefix-only errors from older sessions. Cursor Composer requests use a native `read`/`grep`/`write`/`delete` discipline prompt rather than the generic hashline-tool vocabulary.
15
+ - Alibaba Token Plan streams now allow 600 seconds for the first semantic event, matching observed long-context TTFT above the previous 300-second cutoff. The outer lazy watchdog and both OpenAI transports share one provider fallback; OpenAI Completions also applies it before response headers, and Alibaba SDK connection timeouts from that pre-stream phase are normalized to the typed first-event failure so session retry policy does not replay the request as an unknown timeout.
16
+ - A plain `forbidden` failure no longer mutates credential state. `classifyFallbackTrigger` still returns the same `auth` class for HTTP 401 and 403, but now carries an `authDisposition` refinement of `"credential"` or `"forbidden"`. The refinement reads every code field (`openaiErrorCode`, `anthropicErrorType`, `providerCode`) and orders by specificity: a concrete credential fault wins, a `forbidden` in any field is otherwise terminal (so `{status: 401, providerCode: "forbidden"}` does not rotate), and the status decides only when no auth code is present. `transportFailureFacts` also reads `anthropicErrorType` back from its own key so re-normalizing already-built facts no longer drops it. `streamSimple` consults the disposition at both auth-capture exits — the error-event path and the thrown-error path, the latter unwrapping a nested `error.transportFailure` carrier that the shared `transportFailureFacts` extractor does not dereference — so a forbidden failure never reaches `onAuthError`, and `createAssistantAuthError` now preserves the structured transport facts on the callback error instead of reducing it to a status. The auth gateway's managed-failure bookkeeping likewise stops invalidating a credential on a forbidden response. Previously a single 403 could block an otherwise-healthy credential, and in a multi-credential pool could cycle through and block every row.
17
+ - `AuthStorage` gains `hasRuntimeCredentialSelector()` and `getSessionCredentialRowId()`. The first reports the `--credential` runtime pin, which lives in a different map from the `--api-key` override and previously had no accessor, so callers that must not rotate away from a pinned credential could not see it. The second returns the opaque stored row id for a session's current credential — never an email, account, project, or key material.
18
+
19
+ ## [0.12.8] - 2026-08-02
20
+ ### Added
21
+
22
+ - Added read-only OpenCodex provider discovery with runtime-port resolution, identity-checked health probing, cached `/api/models` catalogs, raw wire model ids, and `/login opencodex` status reprobes without credential persistence.
23
+ - Added the Alibaba Token Plan `deepseek-v4-flash-0731` model with its 1M context, 384K output limit, OpenAI Completions routing, and documented low/high/max reasoning efforts.
24
+
25
+ ### Changed
26
+
27
+ - OpenAI-compatible discovery and OpenAI Completions/Responses transports now preserve query-bearing endpoint routing, including repeated query parameters. Model resolution records whether a provider discovery result was fetched so consumers can distinguish current discovery evidence from cached data.
28
+
29
+ ### Fixed
30
+
31
+ - Closed the two remaining ingress holes behind bare `Request Blocked` failures on OpenAI codex models. (1) The chatgpt.com/backend-api pre-model gate rejects with an HTTP 400 bare-`detail` body (`{"detail": "Request blocked."}`) carrying no `error.*` envelope and no `code=invalid_prompt`, so `parseCodexError` surfaced an unexplained message, `isInvalidPromptError` and the codex non-retryable classification missed it, and the session-level `invalid_prompt` circuit breaker never attempted a repaired resend. `parseCodexError` now reads top-level `detail` (string or `{message}`) bodies and classifies a leading `Request blocked` message without an explicit provider code as `invalid_prompt`, surfacing `Request blocked (code=invalid_prompt)` so every existing invalid_prompt contract engages. (2) Outgoing tool definitions (descriptions and JSON-schema strings) bypassed every request-boundary sanitizer on both the OpenAI Responses and OpenAI-codex-responses transports, so a `<|channel|>`-quoting MCP/skill tool description poisoned every request on the session in a way no history repair could fix. Both `convertTools` paths now neutralize reserved control tokens across the whole tool payload via the shared idempotent zero-width-space insertion (ref openai/codex#35838).
32
+
33
+ ### Fixed
34
+
35
+ - Updated GPT-5.6 Sol, Terra, and Luna to current OpenAI Standard pricing, including Responses API cache-write attribution and full-request long-context pricing above 272K input tokens.
36
+
5
37
  ## [0.12.7] - 2026-07-31
6
38
 
7
39
  ## [0.12.6] - 2026-07-31
@@ -10,7 +10,7 @@
10
10
  import { Database } from "bun:sqlite";
11
11
  import type { Provider } from "./types";
12
12
  import type { CredentialRankingStrategy, UsageLogger, UsageProvider, UsageReport } from "./usage";
13
- import type { OAuthController, OAuthCredentials, OAuthProviderId } from "./utils/oauth/types";
13
+ import type { OAuthController, OAuthCredentials, OAuthLoginOptions, OAuthProviderId } from "./utils/oauth/types";
14
14
  export type ApiKeyCredential = {
15
15
  type: "api_key";
16
16
  key: string;
@@ -290,8 +290,9 @@ export type AuthStorageOptions = {
290
290
  * Resolve a config value (API key, header value, etc.) to an actual value.
291
291
  * - coding-agent injects its resolveConfigValue (supports "!command" syntax via pi-natives)
292
292
  * - Default: checks environment variable first, then treats as literal
293
+ * `cacheScope` changes whenever the provider credential configuration changes.
293
294
  */
294
- configValueResolver?: (config: string) => Promise<string | undefined>;
295
+ configValueResolver?: (config: string, cacheScope?: string) => Promise<string | undefined>;
295
296
  /**
296
297
  * Optional callback fired when AuthStorage automatically disables a
297
298
  * credential because something detected it as no longer usable — today
@@ -389,6 +390,9 @@ export declare class AuthStorage {
389
390
  */
390
391
  close(): void;
391
392
  getGeneration(): number;
393
+ getProviderConfigurationGeneration(provider: string): number;
394
+ getProviderOAuthRefreshGeneration(provider: string): number;
395
+ getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string): string;
392
396
  onGenerationChanged(listener: (generation: number) => void): () => void;
393
397
  offGenerationChanged(listener: (generation: number) => void): void;
394
398
  /**
@@ -430,6 +434,29 @@ export declare class AuthStorage {
430
434
  removeRuntimeApiKey(provider: string): void;
431
435
  /** Whether a provider is currently authenticated by a runtime API-key override. */
432
436
  hasRuntimeApiKey(provider: string): boolean;
437
+ /**
438
+ * Whether credential selection for a provider is pinned to one stored row by
439
+ * a runtime selector (`--credential`).
440
+ *
441
+ * Distinct from {@link AuthStorage.hasRuntimeApiKey}: that reports the
442
+ * `--api-key` override, which lives in a different map and is mutually
443
+ * exclusive with a selector. Callers that must not rotate away from a pinned
444
+ * credential have to consult BOTH.
445
+ */
446
+ hasRuntimeCredentialSelector(provider: string): boolean;
447
+ /**
448
+ * Opaque stored row id of the credential this session is currently using.
449
+ *
450
+ * Deliberately non-identifying: the persisted primary key, never an email,
451
+ * account id, project id, or key material. Callers that need to correlate a
452
+ * credential across a session boundary use this instead of projecting
453
+ * personal metadata.
454
+ *
455
+ * Returns `undefined` when the session has not been routed to a stored
456
+ * credential yet, or when it authenticated through an env key or fallback
457
+ * resolver rather than a stored row.
458
+ */
459
+ getSessionCredentialRowId(provider: string, sessionId?: string): number | undefined;
433
460
  /**
434
461
  * Register a per-provider API key sourced from user configuration
435
462
  * (e.g. `models.yml` `providers.<name>.apiKey`). Higher priority than
@@ -482,11 +509,11 @@ export declare class AuthStorage {
482
509
  * Check if credentials exist for a provider in storage.
483
510
  */
484
511
  has(provider: string): boolean;
512
+ hasAuth(provider: string): boolean;
485
513
  /**
486
- * Check if any form of auth is configured for a provider.
487
- * Unlike getApiKey(), this doesn't refresh OAuth tokens.
514
+ * Check whether configured auth is currently usable without resolving credentials.
488
515
  */
489
- hasAuth(provider: string): boolean;
516
+ hasUsableAuth(provider: string): boolean;
490
517
  /**
491
518
  * Check if OAuth credentials are configured for a provider.
492
519
  */
@@ -521,7 +548,7 @@ export declare class AuthStorage {
521
548
  message: string;
522
549
  placeholder?: string;
523
550
  }) => Promise<string>;
524
- }): Promise<void>;
551
+ }, options?: OAuthLoginOptions): Promise<void>;
525
552
  /**
526
553
  * Logout from a provider.
527
554
  */
@@ -24,6 +24,7 @@ export * from "./providers/mock";
24
24
  export * from "./providers/ollama";
25
25
  export * from "./providers/openai-codex-responses";
26
26
  export * from "./providers/openai-completions";
27
+ export * from "./providers/openai-opencodex-responses";
27
28
  export * from "./providers/openai-responses";
28
29
  export * from "./providers/synthetic";
29
30
  export * from "./rate-limit-utils";
@@ -45,7 +46,7 @@ export * from "./utils/event-stream";
45
46
  export * from "./utils/fallback-transport";
46
47
  export * from "./utils/h2-fetch";
47
48
  export * from "./utils/oauth";
48
- export type { OAuthCredentials, OAuthProvider, OAuthProviderId, OAuthProviderInfo, } from "./utils/oauth/types";
49
+ export type { OAuthCredentials, OAuthLoginOptions, OAuthProvider, OAuthProviderId, OAuthProviderInfo, } from "./utils/oauth/types";
49
50
  export * from "./utils/overflow";
50
51
  export * from "./utils/retry";
51
52
  export * from "./utils/schema";
@@ -44,6 +44,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
44
44
  export interface ModelResolutionResult<TApi extends Api = Api> {
45
45
  models: Model<TApi>[];
46
46
  stale: boolean;
47
+ /** Whether this resolution successfully fetched dynamic models. */
48
+ fetched: boolean;
47
49
  }
48
50
  /**
49
51
  * Stateful facade over provider model resolution.
@@ -0,0 +1,3 @@
1
+ import type { Api, Model, ModelCost } from "./types";
2
+ export declare function getOpenAIModelCost<TApi extends Api>(model: Model<TApi>, inputTokens: number): ModelCost | undefined;
3
+ export declare function applyOpenAIModelPricing<TApi extends Api>(model: Model<TApi>): void;
@@ -1,4 +1,5 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
+ export declare function openCodexModelManagerOptions(): ModelManagerOptions<"openai-responses">;
2
3
  export interface OpenAICodexModelManagerConfig {
3
4
  accessToken?: string;
4
5
  accountId?: string;
@@ -1,26 +1,32 @@
1
+ export declare function isComposerHarnessModel(modelId: string): boolean;
2
+ /** Stable text contract for a local shell rejection caused by Composer file-I/O discipline. */
3
+ export declare const COMPOSER_BASH_POLICY_ERROR_PREFIX = "Composer bash policy blocked repository file I/O.";
4
+ export declare const COMPOSER_BASH_POLICY_ERROR_CODE = "composer-bash-policy:repository-file-io";
5
+ export type ComposerBashPolicyToolSurface = "generic" | "cursor";
1
6
  /**
2
- * Anchor/edit discipline for composer-harness models (xai grok-composer-*,
3
- * cursor composer-*).
4
- *
5
- * Composer models are trained on a proprietary coding-agent harness
6
- * (Cursor / Grok Build) and carry habits that break this agent's hashline
7
- * edit workflow when driven through a generic provider. Observed in live
8
- * sessions with grok-composer-2.5-fast:
9
- *
10
- * - they print files with shell commands (`sed -n`, `cat`, `grep -n`) or
11
- * python heredocs whose output carries NO line anchors, then FABRICATE the
12
- * 2-char anchor hash the edit tool requires (e.g. guessed "617hp" where
13
- * the file had "617ca" → "Edit rejected: N anchors do not match");
14
- * - they mutate files out-of-band via python heredocs (pathlib write_text /
15
- * str.replace), which invalidates every previously seen anchor and defeats
16
- * the read-cache snapshot that powers stale-anchor recovery;
17
- * - they arithmetically renumber anchors after their own edits instead of
18
- * copying them from the latest tool output;
19
- * - they leak reasoning prose into heredoc bodies, producing shell/python
20
- * syntax errors.
21
- *
22
- * This prompt is the per-request countermeasure, pinned ahead of the host
23
- * system prompt on openai-completions, openai-responses, and cursor RPC paths.
7
+ * Format the model-visible policy rejection with a stable marker and the tool
8
+ * vocabulary the model actually receives on this provider surface.
24
9
  */
25
- export declare function isComposerHarnessModel(modelId: string): boolean;
10
+ export declare function formatComposerBashPolicyError(surface?: ComposerBashPolicyToolSurface): string;
11
+ /**
12
+ * Matches both the structured current error and the original prefix so a
13
+ * resumed session can recover after an upgrade without string-version skew.
14
+ */
15
+ export declare function isComposerBashPolicyBlockedError(text: string): boolean;
16
+ /**
17
+ * Matches only errors emitted directly by the current policy implementation.
18
+ * Live recovery must use this strict form so failed shell output that merely
19
+ * quotes a policy error cannot masquerade as the policy gate itself.
20
+ */
21
+ export declare function isCurrentComposerBashPolicyBlockedError(text: string): boolean;
22
+ /** One bounded, tool-enabled retry instruction for generic Composer agent loops. */
23
+ export declare const COMPOSER_BASH_POLICY_RECOVERY_PROMPT: string;
24
+ /** One bounded, tool-enabled retry instruction for Cursor's native remote tool surface. */
25
+ export declare const CURSOR_COMPOSER_BASH_POLICY_RECOVERY_PROMPT: string;
26
26
  export declare const COMPOSER_EDIT_DISCIPLINE_PROMPT = "File-editing discipline for this Composer harness (this OVERRIDES contrary habits from your training):\n\n- Discover file names ONLY with the find tool; search file contents ONLY with the search tool; read file bodies or line ranges ONLY with the read tool. NEVER inspect repository files through shell commands (ls, find, fd, cat, sed, awk, grep, rg, head, tail, less, more) or scripts \u2014 that output carries no hashline anchors and bypasses the agent's safety limits.\n- Modify files ONLY with the edit/write tools. NEVER mutate files through shell redirection, tee, sed -i, perl -pi, inline python/node/bun scripts, or other out-of-band writes \u2014 those writes invalidate every known anchor and break edit recovery.\n- A line anchor (e.g. \"42sr\") is a line number plus a 2-char content hash. You CANNOT compute the hash yourself: copy anchors verbatim from the MOST RECENT read/search/edit output of that exact file. NEVER guess, renumber, or arithmetically shift an anchor.\n- After ANY edit to a file (including your own), anchors you saw earlier are stale. Re-read the edited region, or copy the fresh anchors printed in the edit result, before issuing the next edit.\n- If an edit is rejected with \"anchors do not match\", the rejection message prints the current lines WITH fresh anchors. Retry using exactly those printed anchors.\n- Tool-call arguments must be the exact JSON/schema object requested by the tool. Do not include Markdown, commentary, analysis text, or invented fields inside tool arguments.\n- Use bash only for terminal operations such as tests, builds, package scripts, and git commands. A shell command string must contain only the command itself; NEVER interleave reasoning or commentary into command strings or heredocs.";
27
+ /**
28
+ * Cursor executes a different native tool vocabulary from the generic agent
29
+ * loop. Keep this prompt separate so Composer is never told to call `edit`,
30
+ * `find`, or `search` when those names are unavailable remotely.
31
+ */
32
+ export declare const CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT: string;
@@ -0,0 +1,10 @@
1
+ import type { Model } from "../types";
2
+ export declare const OPENCODEX_DEFAULT_PORT = 10100;
3
+ export declare const OPENCODEX_PROBE_TIMEOUT_MS = 750;
4
+ export declare const OPENCODEX_MODEL_CACHE_TTL_MS: number;
5
+ export interface OpenCodexEndpoint {
6
+ baseUrl: string;
7
+ }
8
+ export declare function resolveOpenCodexEndpoint(signal?: AbortSignal): Promise<OpenCodexEndpoint | undefined>;
9
+ export declare function fetchOpenCodexModels(): Promise<readonly Model<"openai-responses">[] | null>;
10
+ export declare function checkOpenCodexStatus(onProgress?: (message: string) => void): Promise<void>;
@@ -96,6 +96,7 @@ export declare function populateResponsesUsageFromResponse(output: AssistantMess
96
96
  total_tokens?: number | null;
97
97
  input_tokens_details?: {
98
98
  cached_tokens?: number | null;
99
+ cache_write_tokens?: number | null;
99
100
  } | null;
100
101
  output_tokens_details?: {
101
102
  reasoning_tokens?: number | null;
@@ -20,8 +20,8 @@ export declare function setBedrockProviderModule(module: BedrockProviderModule):
20
20
  /**
21
21
  * Resolves the first-event timeout fallback for the outer lazy-stream watchdog.
22
22
  * A configured wrapper-specific fallback (from `LazyStreamLimits`) always wins;
23
- * otherwise providers known to have slow first events get a five-minute floor
24
- * matching their inner provider-level override. Returns `undefined` for
23
+ * otherwise providers known to have slow first events use the same centralized
24
+ * fallback as their inner provider-level watchdog. Returns `undefined` for
25
25
  * providers that should use the shared default.
26
26
  */
27
27
  export declare function resolveLazyStreamFirstEventFallbackMs(provider: string, configuredFallbackMs?: number): number | undefined;
@@ -51,7 +51,7 @@ export interface ThinkingConfig {
51
51
  /** Provider-specific transport used to encode the selected effort. */
52
52
  mode: ThinkingControlMode;
53
53
  }
54
- export type KnownProvider = "alibaba-token-plan" | "amazon-bedrock" | "azure-openai" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "fugu" | "gitlab-duo" | "cursor" | "deepseek" | "deepinfra" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "glm-zcode" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "opengateway" | "bizrouter" | "mara" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "lm-studio";
54
+ export type KnownProvider = "alibaba-token-plan" | "amazon-bedrock" | "azure-openai" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "opencodex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "fugu" | "gitlab-duo" | "cursor" | "deepseek" | "deepinfra" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "glm-zcode" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "opengateway" | "bizrouter" | "mara" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "lm-studio";
55
55
  export type Provider = KnownProvider | string;
56
56
  import type { Effort } from "./model-thinking";
57
57
  /** Token budgets for each thinking level (token-based providers only) */
@@ -827,6 +827,17 @@ export interface ModelRequestTransform {
827
827
  /** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
828
828
  extraBody?: Record<string, unknown>;
829
829
  }
830
+ export interface ModelCost {
831
+ input: number;
832
+ output: number;
833
+ cacheRead: number;
834
+ cacheWrite: number;
835
+ }
836
+ export interface LongContextPricing {
837
+ /** Input-token count above which the long-context rates apply to the full request. */
838
+ threshold: number;
839
+ cost: ModelCost;
840
+ }
830
841
  export interface Model<TApi extends Api = any> {
831
842
  id: string;
832
843
  name: string;
@@ -843,12 +854,9 @@ export interface Model<TApi extends Api = any> {
843
854
  * provider/id heuristics.
844
855
  */
845
856
  output?: ("text" | "image")[];
846
- cost: {
847
- input: number;
848
- output: number;
849
- cacheRead: number;
850
- cacheWrite: number;
851
- };
857
+ cost: ModelCost;
858
+ /** Optional long-context rates selected from the request's total input-token count. */
859
+ longContextPricing?: LongContextPricing;
852
860
  /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
853
861
  premiumMultiplier?: number;
854
862
  contextWindow: number;
@@ -1,7 +1,23 @@
1
1
  export type FallbackTriggerClass = "rate_limit" | "quota" | "auth" | "server" | "unknown" | "other";
2
+ /**
3
+ * Refinement of an `auth` trigger.
4
+ *
5
+ * The transport deliberately collapses HTTP 401 and 403 into a single `auth`
6
+ * class, but the two demand opposite handling: a credential problem may be
7
+ * recoverable by trying a different stored credential, whereas a plain
8
+ * `forbidden` is an authorization or configuration defect that rotation would
9
+ * only hide — it would cycle and block every otherwise-healthy credential.
10
+ *
11
+ * This is a refinement rather than a new {@link FallbackTriggerClass} member so
12
+ * every existing `trigger.class === "auth"` consumer keeps compiling and keeps
13
+ * its current behavior until it explicitly opts into the distinction.
14
+ */
15
+ export type AuthDisposition = "credential" | "forbidden";
2
16
  export interface FallbackTrigger {
3
17
  class: FallbackTriggerClass;
4
18
  retryAfterMs?: number;
19
+ /** Present only when `class === "auth"`. */
20
+ authDisposition?: AuthDisposition;
5
21
  }
6
22
  /** Stable code for streams that time out before producing semantic progress. */
7
23
  export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
@@ -66,3 +82,10 @@ export declare function transportFailureFacts(error: unknown, capturedResponse?:
66
82
  }): TransportFailureFacts | undefined;
67
83
  /** Classifies only typed upstream transport facts without consuming response bodies. */
68
84
  export declare function classifyFallbackTrigger(errorOrFacts: TransportFailureFacts | FallbackTriggerInput | unknown): FallbackTrigger;
85
+ /**
86
+ * True when a failure is an `auth` failure that must NOT rotate credentials.
87
+ *
88
+ * Callers that mutate credential state on auth failures should consult this
89
+ * first so a plain `forbidden` cannot block otherwise-healthy credentials.
90
+ */
91
+ export declare function isForbiddenAuthFailure(errorOrFacts: TransportFailureFacts | FallbackTriggerInput | unknown): boolean;
@@ -3,9 +3,28 @@
3
3
  */
4
4
  import { OAuthCallbackFlow } from "./callback-server";
5
5
  import type { OAuthController, OAuthCredentials } from "./types";
6
+ /**
7
+ * Redirect target for the paste-a-code login. Anthropic renders the
8
+ * authorization code on this page instead of redirecting into this machine, so
9
+ * a gjc running over SSH, in a container, or on a headless box can be paired
10
+ * from a browser that has no route back to `localhost:54545`.
11
+ *
12
+ * Deliberately a hard-coded constant rather than an env/config override: this
13
+ * is where the authorization code is delivered, so making it injectable would
14
+ * turn any writable environment into an auth-code exfiltration channel.
15
+ */
16
+ export declare const ANTHROPIC_MANUAL_REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
17
+ export interface AnthropicOAuthFlowOptions {
18
+ /**
19
+ * Pair by pasting the code Anthropic displays instead of waiting on a local
20
+ * `localhost:54545` callback. Use when the browser completing the login has
21
+ * no network route back to the machine running gjc.
22
+ */
23
+ manualCode?: boolean;
24
+ }
6
25
  export declare class AnthropicOAuthFlow extends OAuthCallbackFlow {
7
26
  #private;
8
- constructor(ctrl: OAuthController);
27
+ constructor(ctrl: OAuthController, options?: AnthropicOAuthFlowOptions);
9
28
  generateAuthUrl(state: string, redirectUri: string): Promise<{
10
29
  url: string;
11
30
  instructions?: string;
@@ -15,7 +34,7 @@ export declare class AnthropicOAuthFlow extends OAuthCallbackFlow {
15
34
  /**
16
35
  * Login with Anthropic OAuth
17
36
  */
18
- export declare function loginAnthropic(ctrl: OAuthController): Promise<OAuthCredentials>;
37
+ export declare function loginAnthropic(ctrl: OAuthController, options?: AnthropicOAuthFlowOptions): Promise<OAuthCredentials>;
19
38
  /**
20
39
  * Refresh Anthropic OAuth token
21
40
  */
@@ -11,6 +11,13 @@ export interface OAuthCallbackFlowOptions {
11
11
  callbackBindHostname?: string;
12
12
  /** Exact redirect URI advertised to the provider; disables port fallback. */
13
13
  redirectUri?: string;
14
+ /**
15
+ * Do not bind a local listener at all. The provider redirects somewhere this
16
+ * process cannot observe (a hosted "copy this code" page, a custom protocol),
17
+ * so the code arrives by paste instead. Requires both `redirectUri` and an
18
+ * `onManualCodeInput` handler on the controller.
19
+ */
20
+ skipCallbackServer?: boolean;
14
21
  }
15
22
  /**
16
23
  * Abstract base class for OAuth flows with local callback servers.
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai";
10
+ export type OAuthProvider = "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
@@ -23,6 +23,17 @@ export interface OAuthProviderInfo {
23
23
  name: string;
24
24
  available: boolean;
25
25
  }
26
+ /** Per-login switches that change how the authorization code is delivered. */
27
+ export interface OAuthLoginOptions {
28
+ /**
29
+ * Pair by pasting the authorization code the provider displays instead of
30
+ * waiting on a local loopback callback. Set when the browser completing the
31
+ * login has no network route back to the machine running gjc (SSH, remote
32
+ * container, headless host). Providers without a paste-a-code redirect
33
+ * ignore it.
34
+ */
35
+ manualCode?: boolean;
36
+ }
26
37
  export interface OAuthController {
27
38
  onAuth?(info: OAuthAuthInfo): void;
28
39
  onProgress?(message: string): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.12.7",
4
+ "version": "0.12.10",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/utils": "0.12.7",
43
+ "@gajae-code/utils": "0.12.10",
44
44
  "openai": "^6.36.0",
45
45
  "partial-json": "^0.1.7",
46
46
  "zod": "4.4.3"
@@ -279,6 +279,12 @@ async function markManagedGatewayCredentialFailure(
279
279
  ): Promise<void> {
280
280
  const trigger = classifyFallbackTrigger(error);
281
281
  try {
282
+ if (trigger.class === "auth" && trigger.authDisposition === "forbidden") {
283
+ // A plain `forbidden` is an authorization or configuration defect.
284
+ // Blocking the credential here would hide it and would cycle through
285
+ // every otherwise-healthy row in a multi-credential pool.
286
+ return;
287
+ }
282
288
  if (trigger.class === "auth") {
283
289
  await storage.invalidateCredentialMatching(model.provider, apiKey, signal);
284
290
  } else if (trigger.class === "quota" || trigger.class === "rate_limit") {