@oh-my-pi/pi-ai 13.5.8 → 13.6.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.6.0] - 2026-03-03
6
+ ### Added
7
+
8
+ - Added Anthropic Foundry gateway mode controlled by `CLAUDE_CODE_USE_FOUNDRY`, with support for `FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_CUSTOM_HEADERS`, and optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`)
9
+ - Added LM Studio provider support with OpenAI-compatible model discovery and OAuth login.
10
+ - Added support for `LM_STUDIO_API_KEY` and `LM_STUDIO_BASE_URL` environment variables for authentication and custom host configuration.
11
+
12
+ ### Changed
13
+
14
+ - Anthropic key resolution now prefers `ANTHROPIC_FOUNDRY_API_KEY` over `ANTHROPIC_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` when Foundry mode is enabled
15
+ - Anthropic auth base-URL fallback now prefers `FOUNDRY_BASE_URL` when `CLAUDE_CODE_USE_FOUNDRY` is enabled
16
+
5
17
  ## [13.5.8] - 2026-03-02
6
18
  ### Fixed
7
19
 
@@ -1503,4 +1515,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
1503
1515
 
1504
1516
  ## [0.9.4] - 2025-11-26
1505
1517
 
1506
- Initial release with multi-provider LLM support.
1518
+ Initial release with multi-provider LLM support.
package/README.md CHANGED
@@ -907,7 +907,7 @@ In Node.js environments, you can set environment variables to avoid passing API
907
907
  | Provider | Environment Variable(s) |
908
908
  | -------------- | ---------------------------------------------------------------------------- |
909
909
  | OpenAI | `OPENAI_API_KEY` |
910
- | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
910
+ | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` (or `ANTHROPIC_FOUNDRY_API_KEY` when `CLAUDE_CODE_USE_FOUNDRY=true`) |
911
911
  | Google | `GEMINI_API_KEY` |
912
912
  | Vertex AI | `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
913
913
  | Mistral | `MISTRAL_API_KEY` |
@@ -936,6 +936,10 @@ In Node.js environments, you can set environment variables to avoid passing API
936
936
  For Cloudflare AI Gateway models, use provider base URL format
937
937
  `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic`.
938
938
 
939
+ For Anthropic Foundry routing, set `CLAUDE_CODE_USE_FOUNDRY=true` plus:
940
+ `FOUNDRY_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, optional `ANTHROPIC_CUSTOM_HEADERS`,
941
+ and optional mTLS material (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`).
942
+
939
943
  Provider endpoint defaults for the current OpenAI-compatible integrations:
940
944
 
941
945
  - Together: `https://api.together.xyz/v1`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "13.5.8",
4
+ "version": "13.6.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@anthropic-ai/sdk": "^0.78",
41
- "@aws-sdk/client-bedrock-runtime": "^3.998",
41
+ "@aws-sdk/client-bedrock-runtime": "^3.1000",
42
42
  "@bufbuild/protobuf": "^2.11",
43
43
  "@google/genai": "^1.43",
44
- "@oh-my-pi/pi-utils": "13.5.8",
44
+ "@oh-my-pi/pi-utils": "13.6.1",
45
45
  "@sinclair/typebox": "^0.34",
46
46
  "@smithy/node-http-handler": "^4.4",
47
47
  "ajv": "^8.18",
@@ -45,6 +45,7 @@ import { loginHuggingface } from "./utils/oauth/huggingface";
45
45
  import { loginKilo } from "./utils/oauth/kilo";
46
46
  import { loginKimi } from "./utils/oauth/kimi";
47
47
  import { loginLiteLLM } from "./utils/oauth/litellm";
48
+ import { loginLmStudio } from "./utils/oauth/lm-studio";
48
49
  import { loginMiniMaxCode, loginMiniMaxCodeCn } from "./utils/oauth/minimax-code";
49
50
  import { loginMoonshot } from "./utils/oauth/moonshot";
50
51
  import { loginNanoGPT } from "./utils/oauth/nanogpt";
@@ -820,6 +821,11 @@ export class AuthStorage {
820
821
  await saveApiKeyCredential(apiKey);
821
822
  return;
822
823
  }
824
+ case "lm-studio": {
825
+ const apiKey = await loginLmStudio(ctrl);
826
+ await saveApiKeyCredential(apiKey);
827
+ return;
828
+ }
823
829
  case "ollama": {
824
830
  const apiKey = await loginOllama(ctrl);
825
831
  if (!apiKey) {
package/src/models.json CHANGED
@@ -3246,7 +3246,8 @@
3246
3246
  "Editor-Version": "vscode/1.107.0",
3247
3247
  "Editor-Plugin-Version": "copilot-chat/0.35.0",
3248
3248
  "Copilot-Integration-Id": "vscode-chat"
3249
- }
3249
+ },
3250
+ "premiumMultiplier": 0.33
3250
3251
  },
3251
3252
  "claude-opus-4.5": {
3252
3253
  "id": "claude-opus-4.5",
@@ -3298,7 +3299,8 @@
3298
3299
  "Editor-Version": "vscode/1.107.0",
3299
3300
  "Editor-Plugin-Version": "copilot-chat/0.35.0",
3300
3301
  "Copilot-Integration-Id": "vscode-chat"
3301
- }
3302
+ },
3303
+ "premiumMultiplier": 3
3302
3304
  },
3303
3305
  "claude-sonnet-4": {
3304
3306
  "id": "claude-sonnet-4",
@@ -3562,7 +3564,8 @@
3562
3564
  "supportsStore": false,
3563
3565
  "supportsDeveloperRole": false,
3564
3566
  "supportsReasoningEffort": false
3565
- }
3567
+ },
3568
+ "premiumMultiplier": 0
3566
3569
  },
3567
3570
  "gpt-5": {
3568
3571
  "id": "gpt-5",
@@ -3772,6 +3775,33 @@
3772
3775
  "Copilot-Integration-Id": "vscode-chat"
3773
3776
  }
3774
3777
  },
3778
+ "gpt-5.3-codex": {
3779
+ "id": "gpt-5.3-codex",
3780
+ "name": "GPT-5.3 Codex",
3781
+ "api": "openai-responses",
3782
+ "provider": "github-copilot",
3783
+ "premiumMultiplier": 1,
3784
+ "baseUrl": "https://api.individual.githubcopilot.com",
3785
+ "reasoning": true,
3786
+ "input": [
3787
+ "text",
3788
+ "image"
3789
+ ],
3790
+ "cost": {
3791
+ "input": 0,
3792
+ "output": 0,
3793
+ "cacheRead": 0,
3794
+ "cacheWrite": 0
3795
+ },
3796
+ "contextWindow": 272000,
3797
+ "maxTokens": 128000,
3798
+ "headers": {
3799
+ "User-Agent": "GitHubCopilotChat/0.35.0",
3800
+ "Editor-Version": "vscode/1.107.0",
3801
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
3802
+ "Copilot-Integration-Id": "vscode-chat"
3803
+ }
3804
+ },
3775
3805
  "grok-code-fast-1": {
3776
3806
  "id": "grok-code-fast-1",
3777
3807
  "name": "Grok Code Fast 1",
@@ -3800,7 +3830,8 @@
3800
3830
  "supportsStore": false,
3801
3831
  "supportsDeveloperRole": false,
3802
3832
  "supportsReasoningEffort": false
3803
- }
3833
+ },
3834
+ "premiumMultiplier": 0.25
3804
3835
  }
3805
3836
  },
3806
3837
  "mistral": {
@@ -19694,7 +19725,7 @@
19694
19725
  "cacheWrite": 0
19695
19726
  },
19696
19727
  "contextWindow": 163840,
19697
- "maxTokens": 163840
19728
+ "maxTokens": 65536
19698
19729
  },
19699
19730
  "deepseek/deepseek-v3.2-exp": {
19700
19731
  "id": "deepseek/deepseek-v3.2-exp",
@@ -19715,6 +19746,25 @@
19715
19746
  "contextWindow": 163840,
19716
19747
  "maxTokens": 65536
19717
19748
  },
19749
+ "essentialai/rnj-1-instruct": {
19750
+ "id": "essentialai/rnj-1-instruct",
19751
+ "name": "EssentialAI: Rnj 1 Instruct",
19752
+ "api": "openai-completions",
19753
+ "provider": "openrouter",
19754
+ "baseUrl": "https://openrouter.ai/api/v1",
19755
+ "reasoning": false,
19756
+ "input": [
19757
+ "text"
19758
+ ],
19759
+ "cost": {
19760
+ "input": 0.15,
19761
+ "output": 0.15,
19762
+ "cacheRead": 0,
19763
+ "cacheWrite": 0
19764
+ },
19765
+ "contextWindow": 32768,
19766
+ "maxTokens": 8888
19767
+ },
19718
19768
  "google/gemini-2.0-flash-001": {
19719
19769
  "id": "google/gemini-2.0-flash-001",
19720
19770
  "name": "Google: Gemini 2.0 Flash",
@@ -19813,7 +19863,7 @@
19813
19863
  "cacheWrite": 0.08333333333333334
19814
19864
  },
19815
19865
  "contextWindow": 1048576,
19816
- "maxTokens": 65535
19866
+ "maxTokens": 65536
19817
19867
  },
19818
19868
  "google/gemini-2.5-flash-preview-09-2025": {
19819
19869
  "id": "google/gemini-2.5-flash-preview-09-2025",
@@ -19913,7 +19963,7 @@
19913
19963
  "cacheWrite": 0.08333333333333334
19914
19964
  },
19915
19965
  "contextWindow": 1048576,
19916
- "maxTokens": 65535
19966
+ "maxTokens": 65536
19917
19967
  },
19918
19968
  "google/gemini-3-pro-preview": {
19919
19969
  "id": "google/gemini-3-pro-preview",
@@ -20089,7 +20139,7 @@
20089
20139
  "cacheWrite": 0
20090
20140
  },
20091
20141
  "contextWindow": 131072,
20092
- "maxTokens": 32768
20142
+ "maxTokens": 131072
20093
20143
  },
20094
20144
  "meta-llama/llama-3-8b-instruct": {
20095
20145
  "id": "meta-llama/llama-3-8b-instruct",
@@ -20281,7 +20331,7 @@
20281
20331
  "cacheWrite": 0
20282
20332
  },
20283
20333
  "contextWindow": 196608,
20284
- "maxTokens": 65536
20334
+ "maxTokens": 196608
20285
20335
  },
20286
20336
  "minimax/minimax-m2.1": {
20287
20337
  "id": "minimax/minimax-m2.1",
@@ -22939,12 +22989,12 @@
22939
22989
  ],
22940
22990
  "cost": {
22941
22991
  "input": 0.25,
22942
- "output": 2,
22992
+ "output": 1,
22943
22993
  "cacheRead": 0,
22944
22994
  "cacheWrite": 0
22945
22995
  },
22946
22996
  "contextWindow": 262144,
22947
- "maxTokens": 65536
22997
+ "maxTokens": 262144
22948
22998
  },
22949
22999
  "qwen/qwen3.5-397b-a17b": {
22950
23000
  "id": "qwen/qwen3.5-397b-a17b",
@@ -23202,6 +23252,25 @@
23202
23252
  "contextWindow": 163840,
23203
23253
  "maxTokens": 65536
23204
23254
  },
23255
+ "upstage/solar-pro-3": {
23256
+ "id": "upstage/solar-pro-3",
23257
+ "name": "Upstage: Solar Pro 3",
23258
+ "api": "openai-completions",
23259
+ "provider": "openrouter",
23260
+ "baseUrl": "https://openrouter.ai/api/v1",
23261
+ "reasoning": true,
23262
+ "input": [
23263
+ "text"
23264
+ ],
23265
+ "cost": {
23266
+ "input": 0.15,
23267
+ "output": 0.6,
23268
+ "cacheRead": 0.015,
23269
+ "cacheWrite": 0
23270
+ },
23271
+ "contextWindow": 128000,
23272
+ "maxTokens": 8888
23273
+ },
23205
23274
  "upstage/solar-pro-3:free": {
23206
23275
  "id": "upstage/solar-pro-3:free",
23207
23276
  "name": "Upstage: Solar Pro 3 (free)",
@@ -23598,13 +23667,13 @@
23598
23667
  "text"
23599
23668
  ],
23600
23669
  "cost": {
23601
- "input": 0.95,
23602
- "output": 2.5500000000000003,
23603
- "cacheRead": 0.19999999999999998,
23670
+ "input": 0.7999999999999999,
23671
+ "output": 2.56,
23672
+ "cacheRead": 0.16,
23604
23673
  "cacheWrite": 0
23605
23674
  },
23606
- "contextWindow": 204800,
23607
- "maxTokens": 131072
23675
+ "contextWindow": 202752,
23676
+ "maxTokens": 8888
23608
23677
  }
23609
23678
  },
23610
23679
  "kilo": {
@@ -29499,6 +29568,25 @@
29499
29568
  "contextWindow": 222222,
29500
29569
  "maxTokens": 8888
29501
29570
  },
29571
+ "upstage/solar-pro-3": {
29572
+ "id": "upstage/solar-pro-3",
29573
+ "name": "Upstage: Solar Pro 3",
29574
+ "api": "openai-completions",
29575
+ "provider": "kilo",
29576
+ "baseUrl": "https://api.kilo.ai/api/gateway",
29577
+ "reasoning": false,
29578
+ "input": [
29579
+ "text"
29580
+ ],
29581
+ "cost": {
29582
+ "input": 0,
29583
+ "output": 0,
29584
+ "cacheRead": 0,
29585
+ "cacheWrite": 0
29586
+ },
29587
+ "contextWindow": 222222,
29588
+ "maxTokens": 8888
29589
+ },
29502
29590
  "writer/palmyra-x5": {
29503
29591
  "id": "writer/palmyra-x5",
29504
29592
  "name": "Writer: Palmyra X5",
@@ -36753,6 +36841,26 @@
36753
36841
  },
36754
36842
  "contextWindow": 1000000,
36755
36843
  "maxTokens": 64000
36844
+ },
36845
+ "gemini-3.1-pro-preview": {
36846
+ "id": "gemini-3.1-pro-preview",
36847
+ "name": "Gemini 3.1 Pro Preview",
36848
+ "api": "google-gemini-cli",
36849
+ "provider": "google-gemini-cli",
36850
+ "baseUrl": "https://cloudcode-pa.googleapis.com",
36851
+ "reasoning": true,
36852
+ "input": [
36853
+ "text",
36854
+ "image"
36855
+ ],
36856
+ "cost": {
36857
+ "input": 0,
36858
+ "output": 0,
36859
+ "cacheRead": 0,
36860
+ "cacheWrite": 0
36861
+ },
36862
+ "contextWindow": 1048576,
36863
+ "maxTokens": 65536
36756
36864
  }
36757
36865
  },
36758
36866
  "google-vertex": {
@@ -17,6 +17,7 @@ import {
17
17
  kiloModelManagerOptions,
18
18
  kimiCodeModelManagerOptions,
19
19
  litellmModelManagerOptions,
20
+ lmStudioModelManagerOptions,
20
21
  mistralModelManagerOptions,
21
22
  moonshotModelManagerOptions,
22
23
  nanoGptModelManagerOptions,
@@ -209,6 +210,13 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
209
210
  config => litellmModelManagerOptions(config),
210
211
  catalog("LiteLLM", ["LITELLM_API_KEY"], { allowUnauthenticated: true }),
211
212
  ),
213
+ catalogDescriptor(
214
+ "lm-studio",
215
+ "llama-3-8b",
216
+ config => lmStudioModelManagerOptions(config),
217
+ catalog("LM Studio", ["LM_STUDIO_API_KEY"], { allowUnauthenticated: true }),
218
+ { allowUnauthenticated: true },
219
+ ),
212
220
  catalogDescriptor(
213
221
  "vllm",
214
222
  "gpt-oss-20b",
@@ -7,6 +7,7 @@ import {
7
7
  type OpenAICompatibleModelMapperContext,
8
8
  type OpenAICompatibleModelRecord,
9
9
  } from "../utils/discovery/openai-compatible";
10
+ import { getGitHubCopilotBaseUrl } from "../utils/oauth/github-copilot";
10
11
 
11
12
  const MODELS_DEV_URL = "https://models.dev/api.json";
12
13
  const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
@@ -796,6 +797,37 @@ export function kimiCodeModelManagerOptions(
796
797
  };
797
798
  }
798
799
 
800
+ // ---------------------------------------------------------------------------
801
+ // 12.5. LM Studio
802
+ // ---------------------------------------------------------------------------
803
+
804
+ export interface LmStudioModelManagerConfig {
805
+ apiKey?: string;
806
+ baseUrl?: string;
807
+ }
808
+
809
+ export function lmStudioModelManagerOptions(
810
+ config?: LmStudioModelManagerConfig,
811
+ ): ModelManagerOptions<"openai-completions"> {
812
+ const apiKey = config?.apiKey;
813
+ const baseUrl = config?.baseUrl ?? Bun.env.LM_STUDIO_BASE_URL ?? "http://127.0.0.1:1234/v1";
814
+ const references = createBundledReferenceMap<"openai-completions">("lm-studio" as any);
815
+ return {
816
+ providerId: "lm-studio",
817
+ fetchDynamicModels: () =>
818
+ fetchOpenAICompatibleModels({
819
+ api: "openai-completions",
820
+ provider: "lm-studio",
821
+ baseUrl,
822
+ apiKey,
823
+ mapModel: (entry, defaults) => {
824
+ const reference = references.get(defaults.id);
825
+ return mapWithBundledReference(entry, defaults, reference);
826
+ },
827
+ }),
828
+ };
829
+ }
830
+
799
831
  // ---------------------------------------------------------------------------
800
832
  // 13. Synthetic
801
833
  // ---------------------------------------------------------------------------
@@ -1255,7 +1287,11 @@ function extractCopilotLimits(entry: OpenAICompatibleModelRecord): {
1255
1287
 
1256
1288
  export function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {
1257
1289
  const apiKey = config?.apiKey;
1258
- const baseUrl = config?.baseUrl ?? "https://api.individual.githubcopilot.com";
1290
+ const configuredBaseUrl = config?.baseUrl ?? "https://api.individual.githubcopilot.com";
1291
+ const baseUrl =
1292
+ apiKey?.includes("proxy-ep=") && configuredBaseUrl.includes("githubcopilot.com")
1293
+ ? getGitHubCopilotBaseUrl(apiKey)
1294
+ : configuredBaseUrl;
1259
1295
  const references = createBundledReferenceMap<Api>("github-copilot");
1260
1296
  const globalReferences = createGlobalReferenceMap();
1261
1297
  return {
@@ -1,4 +1,5 @@
1
1
  import * as nodeCrypto from "node:crypto";
2
+ import * as fs from "node:fs";
2
3
  import * as tls from "node:tls";
3
4
  import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
4
5
  import type {
@@ -6,7 +7,7 @@ import type {
6
7
  MessageCreateParamsStreaming,
7
8
  MessageParam,
8
9
  } from "@anthropic-ai/sdk/resources/messages";
9
- import { abortableSleep } from "@oh-my-pi/pi-utils";
10
+ import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils";
10
11
  import { calculateCost } from "../models";
11
12
  import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
12
13
  import type {
@@ -33,8 +34,8 @@ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-ins
33
34
  import { parseStreamingJson } from "../utils/json-parse";
34
35
  import {
35
36
  buildCopilotDynamicHeaders,
36
- getCopilotInitiatorOverride,
37
37
  hasCopilotVisionInput,
38
+ resolveGitHubCopilotBaseUrl,
38
39
  } from "./github-copilot-headers";
39
40
  import { transformMessages } from "./transform-messages";
40
41
 
@@ -381,26 +382,135 @@ export type AnthropicClientOptionsResult = {
381
382
 
382
383
  const CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
383
384
 
385
+ type FoundryTlsOptions = {
386
+ ca?: string | string[];
387
+ cert?: string;
388
+ key?: string;
389
+ };
390
+
391
+ function isFoundryEnabled(): boolean {
392
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
393
+ if (!value) return false;
394
+ const normalized = value.trim().toLowerCase();
395
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
396
+ }
397
+
398
+ function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
399
+ const trimmed = baseUrl?.trim();
400
+ return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
401
+ }
402
+
403
+ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
404
+ if (model.provider === "github-copilot") {
405
+ return normalizeBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
406
+ }
407
+ if (model.provider === "anthropic" && isFoundryEnabled()) {
408
+ const foundryBaseUrl = normalizeBaseUrl($env.FOUNDRY_BASE_URL);
409
+ if (foundryBaseUrl) {
410
+ return foundryBaseUrl;
411
+ }
412
+ }
413
+ if (model.provider === "anthropic") {
414
+ return normalizeBaseUrl(model.baseUrl) ?? "https://api.anthropic.com";
415
+ }
416
+ return normalizeBaseUrl(model.baseUrl);
417
+ }
418
+
419
+ function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
420
+ const source = rawHeaders?.trim();
421
+ if (!source) return undefined;
422
+
423
+ const parsed: Record<string, string> = {};
424
+ for (const token of source.split(/\r?\n|,/)) {
425
+ const entry = token.trim();
426
+ if (!entry) continue;
427
+ const separatorIndex = entry.indexOf(":");
428
+ if (separatorIndex <= 0) continue;
429
+ const key = entry.slice(0, separatorIndex).trim();
430
+ const value = entry.slice(separatorIndex + 1).trim();
431
+ if (!key || !value) continue;
432
+ parsed[key] = value;
433
+ }
434
+
435
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
436
+ }
437
+
438
+ function resolveAnthropicCustomHeaders(model: Model<"anthropic-messages">): Record<string, string> | undefined {
439
+ if (model.provider !== "anthropic") return undefined;
440
+ if (!isFoundryEnabled()) return undefined;
441
+ return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
442
+ }
443
+
444
+ function looksLikeFilePath(value: string): boolean {
445
+ return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
446
+ }
447
+
448
+ function resolvePemValue(value: string | undefined, name: string): string | undefined {
449
+ const trimmed = value?.trim();
450
+ if (!trimmed) return undefined;
451
+
452
+ const inline = trimmed.replace(/\\n/g, "\n");
453
+ if (inline.includes("-----BEGIN")) {
454
+ return inline;
455
+ }
456
+
457
+ if (looksLikeFilePath(trimmed)) {
458
+ try {
459
+ return fs.readFileSync(trimmed, "utf8");
460
+ } catch (error) {
461
+ if (isEnoent(error)) {
462
+ throw new Error(`${name} path does not exist: ${trimmed}`);
463
+ }
464
+ throw error;
465
+ }
466
+ }
467
+
468
+ return inline;
469
+ }
470
+
471
+ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
472
+ if (model.provider !== "anthropic") return undefined;
473
+ if (!isFoundryEnabled()) return undefined;
474
+
475
+ const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
476
+ const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
477
+ const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
478
+
479
+ if ((cert && !key) || (!cert && key)) {
480
+ throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
481
+ }
482
+
483
+ const options: FoundryTlsOptions = {};
484
+ if (ca) options.ca = [...tls.rootCertificates, ca];
485
+ if (cert) options.cert = cert;
486
+ if (key) options.key = key;
487
+ return Object.keys(options).length > 0 ? options : undefined;
488
+ }
489
+
384
490
  function buildClaudeCodeTlsFetchOptions(
385
491
  model: Model<"anthropic-messages">,
492
+ baseUrl: string | undefined,
386
493
  ): AnthropicSdkClientOptions["fetchOptions"] | undefined {
387
494
  if (model.provider !== "anthropic") return undefined;
388
- if (!model.baseUrl) return undefined;
495
+ if (!baseUrl) return undefined;
389
496
 
390
497
  let serverName: string;
391
498
  try {
392
- serverName = new URL(model.baseUrl).hostname;
499
+ serverName = new URL(baseUrl).hostname;
393
500
  } catch {
394
501
  return undefined;
395
502
  }
396
503
 
397
504
  if (!serverName) return undefined;
398
505
 
506
+ const foundryTlsOptions = resolveFoundryTlsOptions(model);
507
+
399
508
  return {
400
509
  tls: {
401
510
  rejectUnauthorized: true,
402
511
  serverName,
403
512
  ...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
513
+ ...(foundryTlsOptions ?? {}),
404
514
  },
405
515
  };
406
516
  }
@@ -450,6 +560,15 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
450
560
  const startTime = Date.now();
451
561
  let firstTokenTime: number | undefined;
452
562
 
563
+ const copilotDynamicHeaders =
564
+ model.provider === "github-copilot"
565
+ ? buildCopilotDynamicHeaders({
566
+ messages: context.messages,
567
+ hasImages: hasCopilotVisionInput(context.messages),
568
+ premiumMultiplier: model.premiumMultiplier,
569
+ headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
570
+ })
571
+ : undefined;
453
572
  const output: AssistantMessage = {
454
573
  role: "assistant",
455
574
  content: [],
@@ -471,20 +590,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
471
590
 
472
591
  try {
473
592
  const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
474
-
475
- let copilotDynamicHeaders: Record<string, string> | undefined;
476
- if (model.provider === "github-copilot") {
477
- const hasImages = hasCopilotVisionInput(context.messages);
478
- const initiatorOverride = getCopilotInitiatorOverride({
479
- ...(model.headers ?? {}),
480
- ...(options?.headers ?? {}),
481
- });
482
- copilotDynamicHeaders = buildCopilotDynamicHeaders({
483
- messages: context.messages,
484
- hasImages,
485
- initiatorOverride,
486
- });
487
- }
593
+ const baseUrl = resolveAnthropicBaseUrl(model, apiKey) ?? "https://api.anthropic.com";
488
594
 
489
595
  const { client, isOAuthToken } = createClient(model, {
490
596
  model,
@@ -493,17 +599,17 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
493
599
  stream: true,
494
600
  interleavedThinking: options?.interleavedThinking ?? true,
495
601
  headers: options?.headers,
496
- dynamicHeaders: copilotDynamicHeaders,
602
+ dynamicHeaders: copilotDynamicHeaders?.headers,
497
603
  isOAuth: options?.isOAuth,
498
604
  });
499
- const params = buildParams(model, context, isOAuthToken, options);
605
+ const params = buildParams(model, baseUrl, context, isOAuthToken, options);
500
606
  options?.onPayload?.(params);
501
607
  rawRequestDump = {
502
608
  provider: model.provider,
503
609
  api: output.api,
504
610
  model: model.id,
505
611
  method: "POST",
506
- url: `${model.baseUrl ?? "https://api.anthropic.com"}/v1/messages`,
612
+ url: `${baseUrl}/v1/messages`,
507
613
  body: params,
508
614
  };
509
615
 
@@ -517,6 +623,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
517
623
  let started = false;
518
624
  do {
519
625
  const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
626
+ if (copilotDynamicHeaders && output.usage.premiumRequests === undefined) {
627
+ output.usage.premiumRequests = copilotDynamicHeaders.premiumRequests;
628
+ }
520
629
 
521
630
  try {
522
631
  for await (const event of anthropicStream) {
@@ -830,8 +939,9 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
830
939
  isOAuth,
831
940
  } = args;
832
941
  const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
833
-
834
- const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model);
942
+ const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
943
+ const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
944
+ const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
835
945
  if (model.provider === "github-copilot") {
836
946
  const betaFeatures = [...extraBetas];
837
947
  if (interleavedThinking) {
@@ -853,7 +963,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
853
963
  isOAuthToken: false,
854
964
  apiKey: null,
855
965
  authToken: apiKey,
856
- baseURL: model.baseUrl,
966
+ baseURL: baseUrl,
857
967
  maxRetries: 5,
858
968
  dangerouslyAllowBrowser: true,
859
969
  defaultHeaders,
@@ -868,18 +978,18 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
868
978
 
869
979
  const defaultHeaders = buildAnthropicHeaders({
870
980
  apiKey,
871
- baseUrl: model.baseUrl,
981
+ baseUrl,
872
982
  isOAuth: oauthToken,
873
983
  extraBetas: betaFeatures,
874
984
  stream,
875
- modelHeaders: mergeHeaders(model.headers, headers, dynamicHeaders),
985
+ modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
876
986
  });
877
987
 
878
988
  return {
879
989
  isOAuthToken: oauthToken,
880
990
  apiKey: oauthToken ? null : apiKey,
881
991
  authToken: oauthToken ? apiKey : undefined,
882
- baseURL: model.baseUrl,
992
+ baseURL: baseUrl,
883
993
  maxRetries: 5,
884
994
  dangerouslyAllowBrowser: true,
885
995
  defaultHeaders,
@@ -1152,11 +1262,12 @@ function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreak
1152
1262
  }
1153
1263
  function buildParams(
1154
1264
  model: Model<"anthropic-messages">,
1265
+ baseUrl: string,
1155
1266
  context: Context,
1156
1267
  isOAuthToken: boolean,
1157
1268
  options?: AnthropicOptions,
1158
1269
  ): MessageCreateParamsStreaming {
1159
- const { cacheControl } = getCacheControl(model.baseUrl, options?.cacheRetention);
1270
+ const { cacheControl } = getCacheControl(baseUrl, options?.cacheRetention);
1160
1271
  const params: AnthropicSamplingParams = {
1161
1272
  model: model.id,
1162
1273
  messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
@@ -1,10 +1,25 @@
1
1
  import type { Message } from "../types";
2
-
2
+ import { getGitHubCopilotBaseUrl } from "../utils/oauth/github-copilot";
3
3
  /**
4
4
  * Infer whether the current request to Copilot is user-initiated or agent-initiated.
5
5
  * Accepts `unknown[]` because providers may pass pre-converted message shapes.
6
6
  */
7
- export function inferCopilotInitiator(messages: unknown[]): "user" | "agent" {
7
+ export type CopilotInitiator = "user" | "agent";
8
+ export type CopilotPremiumRequests = number;
9
+ export type CopilotDynamicHeaders = {
10
+ headers: Record<string, string>;
11
+ initiator: CopilotInitiator;
12
+ premiumRequests: CopilotPremiumRequests;
13
+ };
14
+ export function resolveGitHubCopilotBaseUrl(
15
+ baseUrl: string | undefined,
16
+ apiKey: string | undefined,
17
+ ): string | undefined {
18
+ if (!apiKey?.includes("proxy-ep=")) return baseUrl;
19
+ if (baseUrl && !baseUrl.includes("githubcopilot.com")) return baseUrl;
20
+ return getGitHubCopilotBaseUrl(apiKey);
21
+ }
22
+ export function inferCopilotInitiator(messages: unknown[]): CopilotInitiator {
8
23
  if (messages.length === 0) return "user";
9
24
 
10
25
  const last = messages[messages.length - 1] as Record<string, unknown>;
@@ -50,10 +65,10 @@ export function hasCopilotVisionInput(messages: Message[]): boolean {
50
65
  * Resolve an explicitly configured Copilot initiator header, if present.
51
66
  * Handles case-insensitive X-Initiator keys and returns the last valid value.
52
67
  */
53
- export function getCopilotInitiatorOverride(headers: Record<string, string> | undefined): "user" | "agent" | undefined {
68
+ export function getCopilotInitiatorOverride(headers: Record<string, string> | undefined): CopilotInitiator | undefined {
54
69
  if (!headers) return undefined;
55
70
 
56
- let override: "user" | "agent" | undefined;
71
+ let override: CopilotInitiator | undefined;
57
72
  for (const [key, value] of Object.entries(headers)) {
58
73
  if (key.toLowerCase() !== "x-initiator") continue;
59
74
  const normalized = value.trim().toLowerCase();
@@ -64,6 +79,30 @@ export function getCopilotInitiatorOverride(headers: Record<string, string> | un
64
79
 
65
80
  return override;
66
81
  }
82
+
83
+ export type CopilotPlanTier = "free" | "paid";
84
+
85
+ function normalizeCopilotPlanTier(planTier: string | undefined): CopilotPlanTier {
86
+ if (planTier === "paid") return "paid";
87
+ return "free";
88
+ }
89
+ export function getCopilotPremiumMultiplier(premiumMultiplier: number | undefined, planTier?: string): number {
90
+ const normalizedMultiplier = premiumMultiplier ?? 1;
91
+ if (normalizeCopilotPlanTier(planTier) === "free" && normalizedMultiplier === 0) {
92
+ return 1;
93
+ }
94
+ return normalizedMultiplier;
95
+ }
96
+
97
+ export function getCopilotPremiumRequests(params: {
98
+ initiator: CopilotInitiator;
99
+ premiumMultiplier?: number;
100
+ planTier?: string;
101
+ }): CopilotPremiumRequests {
102
+ if (params.initiator === "agent") return 0;
103
+ return getCopilotPremiumMultiplier(params.premiumMultiplier, params.planTier);
104
+ }
105
+
67
106
  /**
68
107
  * Build dynamic Copilot headers that vary per-request.
69
108
  * Static headers (User-Agent, Editor-Version, etc.) come from model.headers.
@@ -71,10 +110,15 @@ export function getCopilotInitiatorOverride(headers: Record<string, string> | un
71
110
  export function buildCopilotDynamicHeaders(params: {
72
111
  messages: unknown[];
73
112
  hasImages: boolean;
74
- initiatorOverride?: "user" | "agent";
75
- }): Record<string, string> {
113
+ premiumMultiplier?: number;
114
+ headers?: Record<string, string>;
115
+ initiatorOverride?: CopilotInitiator;
116
+ planTier?: string;
117
+ }): CopilotDynamicHeaders {
118
+ const initiator =
119
+ params.initiatorOverride ?? getCopilotInitiatorOverride(params.headers) ?? inferCopilotInitiator(params.messages);
76
120
  const headers: Record<string, string> = {
77
- "X-Initiator": params.initiatorOverride ?? inferCopilotInitiator(params.messages),
121
+ "X-Initiator": initiator,
78
122
  "Openai-Intent": "conversation-edits",
79
123
  };
80
124
 
@@ -82,5 +126,13 @@ export function buildCopilotDynamicHeaders(params: {
82
126
  headers["Copilot-Vision-Request"] = "true";
83
127
  }
84
128
 
85
- return headers;
129
+ return {
130
+ headers,
131
+ initiator,
132
+ premiumRequests: getCopilotPremiumRequests({
133
+ initiator,
134
+ premiumMultiplier: params.premiumMultiplier,
135
+ planTier: params.planTier,
136
+ }),
137
+ };
86
138
  }
@@ -24,7 +24,7 @@ const GEMINI_TIER_MAP: Array<{ tier: string; models: string[] }> = [
24
24
  },
25
25
  {
26
26
  tier: "Pro",
27
- models: ["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3-pro", "gemini-1.5-pro"],
27
+ models: ["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3-pro", "gemini-1.5-pro"],
28
28
  },
29
29
  ];
30
30
 
@@ -35,8 +35,8 @@ import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
35
35
  import { mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
36
36
  import {
37
37
  buildCopilotDynamicHeaders,
38
- getCopilotInitiatorOverride,
39
38
  hasCopilotVisionInput,
39
+ resolveGitHubCopilotBaseUrl,
40
40
  } from "./github-copilot-headers";
41
41
  import { transformMessages } from "./transform-messages";
42
42
 
@@ -188,7 +188,12 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
188
188
 
189
189
  try {
190
190
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
191
- const client = await createClient(model, context, apiKey, options?.headers);
191
+ const { client, copilotPremiumRequests, baseUrl } = await createClient(
192
+ model,
193
+ context,
194
+ apiKey,
195
+ options?.headers,
196
+ );
192
197
  const params = buildParams(model, context, options);
193
198
  options?.onPayload?.(params);
194
199
  rawRequestDump = {
@@ -196,10 +201,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
196
201
  api: output.api,
197
202
  model: model.id,
198
203
  method: "POST",
199
- url: `${model.baseUrl ?? "https://api.openai.com/v1"}/chat/completions`,
204
+ url: `${baseUrl ?? "https://api.openai.com/v1"}/chat/completions`,
200
205
  body: params,
201
206
  };
202
207
  const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
208
+ if (copilotPremiumRequests !== undefined) output.usage.premiumRequests = copilotPremiumRequests;
203
209
  stream.push({ type: "start", partial: output });
204
210
 
205
211
  let currentBlock: TextContent | ThinkingContent | (ToolCall & { partialArgs?: string }) | null = null;
@@ -340,6 +346,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
340
346
  // Compute totalTokens ourselves since we add reasoning_tokens to output
341
347
  // and some providers (e.g., Groq) don't include them in total_tokens
342
348
  totalTokens: input + outputTokens + cachedTokens,
349
+ ...(copilotPremiumRequests !== undefined ? { premiumRequests: copilotPremiumRequests } : {}),
343
350
  cost: {
344
351
  input: 0,
345
352
  output: 0,
@@ -510,23 +517,32 @@ async function createClient(
510
517
  if (model.provider === "kimi-code") {
511
518
  headers = { ...(await getKimiCommonHeaders()), ...headers };
512
519
  }
520
+ let copilotPremiumRequests: number | undefined;
521
+
522
+ let baseUrl = model.baseUrl;
513
523
  if (model.provider === "github-copilot") {
514
524
  const hasImages = hasCopilotVisionInput(context.messages);
515
- const copilotHeaders = buildCopilotDynamicHeaders({
525
+ const copilot = buildCopilotDynamicHeaders({
516
526
  messages: context.messages,
517
527
  hasImages,
518
- initiatorOverride: getCopilotInitiatorOverride(headers),
528
+ premiumMultiplier: model.premiumMultiplier,
529
+ headers,
519
530
  });
520
- Object.assign(headers, copilotHeaders);
531
+ Object.assign(headers, copilot.headers);
532
+ copilotPremiumRequests = copilot.premiumRequests;
533
+ baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl;
521
534
  }
522
-
523
- return new OpenAI({
524
- apiKey,
525
- baseURL: model.baseUrl,
526
- dangerouslyAllowBrowser: true,
527
- maxRetries: 5,
528
- defaultHeaders: headers,
529
- });
535
+ return {
536
+ client: new OpenAI({
537
+ apiKey,
538
+ baseURL: baseUrl,
539
+ dangerouslyAllowBrowser: true,
540
+ maxRetries: 5,
541
+ defaultHeaders: headers,
542
+ }),
543
+ copilotPremiumRequests,
544
+ baseUrl,
545
+ };
530
546
  }
531
547
 
532
548
  function buildParams(model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions) {
@@ -36,8 +36,8 @@ import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
36
36
  import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
37
37
  import {
38
38
  buildCopilotDynamicHeaders,
39
- getCopilotInitiatorOverride,
40
39
  hasCopilotVisionInput,
40
+ resolveGitHubCopilotBaseUrl,
41
41
  } from "./github-copilot-headers";
42
42
  import { transformMessages } from "./transform-messages";
43
43
 
@@ -113,7 +113,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
113
113
  try {
114
114
  // Create OpenAI client
115
115
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
116
- const client = createClient(model, context, apiKey, options?.headers);
116
+ const { client, copilotPremiumRequests, baseUrl } = createClient(model, context, apiKey, options?.headers);
117
117
  const params = buildParams(model, context, options);
118
118
  options?.onPayload?.(params);
119
119
  rawRequestDump = {
@@ -121,13 +121,14 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
121
121
  api: output.api,
122
122
  model: model.id,
123
123
  method: "POST",
124
- url: `${model.baseUrl ?? "https://api.openai.com/v1"}/responses`,
124
+ url: `${baseUrl ?? "https://api.openai.com/v1"}/responses`,
125
125
  body: params,
126
126
  };
127
127
  const openaiStream = await client.responses.create(
128
128
  params,
129
129
  options?.signal ? { signal: options.signal } : undefined,
130
130
  );
131
+ if (copilotPremiumRequests !== undefined) output.usage.premiumRequests = copilotPremiumRequests;
131
132
  stream.push({ type: "start", partial: output });
132
133
 
133
134
  let currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null;
@@ -332,6 +333,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
332
333
  cacheRead: cachedTokens,
333
334
  cacheWrite: 0,
334
335
  totalTokens: response.usage.total_tokens || 0,
336
+ ...(copilotPremiumRequests !== undefined ? { premiumRequests: copilotPremiumRequests } : {}),
335
337
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
336
338
  };
337
339
  }
@@ -392,23 +394,32 @@ function createClient(
392
394
  }
393
395
 
394
396
  const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
397
+ let copilotPremiumRequests: number | undefined;
398
+
399
+ let baseUrl = model.baseUrl;
395
400
  if (model.provider === "github-copilot") {
396
401
  const hasImages = hasCopilotVisionInput(context.messages);
397
- const copilotHeaders = buildCopilotDynamicHeaders({
402
+ const copilot = buildCopilotDynamicHeaders({
398
403
  messages: context.messages,
399
404
  hasImages,
400
- initiatorOverride: getCopilotInitiatorOverride(headers),
405
+ premiumMultiplier: model.premiumMultiplier,
406
+ headers,
401
407
  });
402
- Object.assign(headers, copilotHeaders);
408
+ Object.assign(headers, copilot.headers);
409
+ copilotPremiumRequests = copilot.premiumRequests;
410
+ baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl;
403
411
  }
404
-
405
- return new OpenAI({
406
- apiKey,
407
- baseURL: model.baseUrl,
408
- dangerouslyAllowBrowser: true,
409
- maxRetries: 5,
410
- defaultHeaders: headers,
411
- });
412
+ return {
413
+ client: new OpenAI({
414
+ apiKey,
415
+ baseURL: baseUrl,
416
+ dangerouslyAllowBrowser: true,
417
+ maxRetries: 5,
418
+ defaultHeaders: headers,
419
+ }),
420
+ copilotPremiumRequests,
421
+ baseUrl,
422
+ };
412
423
  }
413
424
 
414
425
  function buildParams(model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions) {
package/src/stream.ts CHANGED
@@ -53,6 +53,13 @@ function hasVertexAdcCredentials(): boolean {
53
53
 
54
54
  type KeyResolver = string | (() => string | undefined);
55
55
 
56
+ function isFoundryEnabled(): boolean {
57
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
58
+ if (!value) return false;
59
+ const normalized = value.trim().toLowerCase();
60
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
61
+ }
62
+
56
63
  const serviceProviderMap: Record<string, KeyResolver> = {
57
64
  openai: "OPENAI_API_KEY",
58
65
  google: "GEMINI_API_KEY",
@@ -77,8 +84,11 @@ const serviceProviderMap: Record<string, KeyResolver> = {
77
84
  kagi: "KAGI_API_KEY",
78
85
  // GitHub Copilot uses GitHub personal access token
79
86
  "github-copilot": () => $pickenv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
80
- // ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY
81
- anthropic: () => $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
87
+ // Foundry mode optionally switches Anthropic auth to enterprise gateway credentials.
88
+ anthropic: () =>
89
+ isFoundryEnabled()
90
+ ? $pickenv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
91
+ : $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
82
92
  "gitlab-duo": "GITLAB_TOKEN",
83
93
  // Vertex AI uses Application Default Credentials, not API keys.
84
94
  // Auth is configured via `gcloud auth application-default login`.
@@ -117,6 +127,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
117
127
  moonshot: "MOONSHOT_API_KEY",
118
128
  nvidia: "NVIDIA_API_KEY",
119
129
  nanogpt: "NANO_GPT_API_KEY",
130
+ "lm-studio": "LM_STUDIO_API_KEY",
120
131
  ollama: "OLLAMA_API_KEY",
121
132
  qianfan: "QIANFAN_API_KEY",
122
133
  "qwen-portal": () => $pickenv("QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"),
@@ -563,7 +574,7 @@ function mapOptionsForApi<TApi extends Api>(
563
574
  const googleModel = model as Model<"google-generative-ai">;
564
575
  const effort = clampReasoning(options.reasoning)!;
565
576
 
566
- // Gemini 3 models use thinkingLevel exclusively instead of thinkingBudget.
577
+ // Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
567
578
  // https://ai.google.dev/gemini-api/docs/thinking#set-budget
568
579
  if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel)) {
569
580
  return {
@@ -597,8 +608,8 @@ function mapOptionsForApi<TApi extends Api>(
597
608
 
598
609
  const effort = clampReasoning(options.reasoning)!;
599
610
 
600
- // Gemini 3 models use thinkingLevel instead of thinkingBudget
601
- if (model.id.includes("3-pro") || model.id.includes("3-flash")) {
611
+ // Gemini 3+ models use thinkingLevel instead of thinkingBudget
612
+ if (isGemini3ProModelId(model.id) || isGemini3FlashModelId(model.id)) {
602
613
  return {
603
614
  ...base,
604
615
  thinking: {
@@ -688,14 +699,22 @@ function mapOptionsForApi<TApi extends Api>(
688
699
 
689
700
  type ClampedThinkingLevel = Exclude<ThinkingLevel, "xhigh">;
690
701
 
702
+ function isGemini3ProModelId(modelId: string): boolean {
703
+ return /3(?:\.\d+)?-pro/.test(modelId);
704
+ }
705
+
706
+ function isGemini3FlashModelId(modelId: string): boolean {
707
+ return /3(?:\.\d+)?-flash/.test(modelId);
708
+ }
709
+
691
710
  function isGemini3ProModel(model: Model<"google-generative-ai">): boolean {
692
- // Covers gemini-3-pro, gemini-3-pro-preview, and possible other prefixed ids in the future
693
- return model.id.includes("3-pro");
711
+ // Covers gemini-3-pro, gemini-3-pro-preview, gemini-3.1-pro-preview, and future 3.x variants
712
+ return isGemini3ProModelId(model.id);
694
713
  }
695
714
 
696
715
  function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean {
697
- // Covers gemini-3-flash, gemini-3-flash-preview, and possible other prefixed ids in the future
698
- return model.id.includes("3-flash");
716
+ // Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, and future 3.x variants
717
+ return isGemini3FlashModelId(model.id);
699
718
  }
700
719
 
701
720
  function getGemini3ThinkingLevel(
@@ -727,7 +746,7 @@ function getGemini3ThinkingLevel(
727
746
  }
728
747
 
729
748
  function getGeminiCliThinkingLevel(effort: ClampedThinkingLevel, modelId: string): GoogleThinkingLevel {
730
- if (modelId.includes("3-pro")) {
749
+ if (isGemini3ProModelId(modelId)) {
731
750
  // Gemini 3 Pro only supports LOW/HIGH (for now)
732
751
  switch (effort) {
733
752
  case "minimal":
package/src/types.ts CHANGED
@@ -104,7 +104,8 @@ export type KnownProvider =
104
104
  | "together"
105
105
  | "venice"
106
106
  | "vllm"
107
- | "xiaomi";
107
+ | "xiaomi"
108
+ | "lm-studio";
108
109
  export type Provider = KnownProvider | string;
109
110
 
110
111
  export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -243,6 +244,7 @@ export interface Usage {
243
244
  cacheRead: number;
244
245
  cacheWrite: number;
245
246
  totalTokens: number;
247
+ premiumRequests?: number;
246
248
  cost: {
247
249
  input: number;
248
250
  output: number;
@@ -438,6 +440,8 @@ export interface Model<TApi extends Api = any> {
438
440
  cacheRead: number; // $/million tokens
439
441
  cacheWrite: number; // $/million tokens
440
442
  };
443
+ /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
444
+ premiumMultiplier?: number;
441
445
  contextWindow: number;
442
446
  maxTokens: number;
443
447
  headers?: Record<string, string>;
@@ -4,7 +4,7 @@
4
4
  * 3-tier auth resolution:
5
5
  * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
6
6
  * 2. OAuth credentials in ~/.omp/agent/agent.db (with expiry check)
7
- * 3. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
7
+ * 3. Generic Anthropic fallback (Foundry-aware key/base URL resolution)
8
8
  */
9
9
  import { $env, getAgentDbPath } from "@oh-my-pi/pi-utils";
10
10
  import { type AuthCredential, AuthCredentialStore } from "../auth-storage";
@@ -29,6 +29,26 @@ export interface AnthropicOAuthCredential {
29
29
 
30
30
  const DEFAULT_BASE_URL = "https://api.anthropic.com";
31
31
 
32
+ function isFoundryEnabled(): boolean {
33
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
34
+ if (!value) return false;
35
+ const normalized = value.trim().toLowerCase();
36
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
37
+ }
38
+
39
+ function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
40
+ const trimmed = baseUrl?.trim();
41
+ return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
42
+ }
43
+ function resolveAnthropicBaseUrlFromEnv(): string | undefined {
44
+ if (isFoundryEnabled()) {
45
+ const foundryBaseUrl = normalizeBaseUrl($env.FOUNDRY_BASE_URL);
46
+ if (foundryBaseUrl) return foundryBaseUrl;
47
+ }
48
+ const anthropicBaseUrl = normalizeBaseUrl($env.ANTHROPIC_BASE_URL);
49
+ return anthropicBaseUrl || undefined;
50
+ }
51
+
32
52
  /**
33
53
  * Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
34
54
  * @param apiKey - The API key to check
@@ -81,10 +101,11 @@ async function readAnthropicOAuthCredentials(store?: AuthCredentialStore): Promi
81
101
  }
82
102
 
83
103
  /**
84
- * Finds Anthropic auth config using 3-tier priority:
104
+ * Finds Anthropic auth config using priority:
85
105
  * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
86
- * 2. OAuth in agent.db (with 5-minute expiry buffer)
87
- * 3. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
106
+ * 2. ANTHROPIC_FOUNDRY_API_KEY override when Foundry mode is enabled
107
+ * 3. OAuth in agent.db (with 5-minute expiry buffer)
108
+ * 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
88
109
  * @param store - Optional credential store (creates one from default db path if not provided)
89
110
  * @returns The first valid auth configuration found, or null if none available
90
111
  */
@@ -100,7 +121,16 @@ export async function findAnthropicAuth(store?: AuthCredentialStore): Promise<An
100
121
  };
101
122
  }
102
123
 
103
- // 2. OAuth credentials in agent.db (with 5-minute expiry buffer)
124
+ // 2. Foundry explicit env override
125
+ const foundryApiKey = isFoundryEnabled() ? $env.ANTHROPIC_FOUNDRY_API_KEY?.trim() : undefined;
126
+ if (foundryApiKey) {
127
+ return {
128
+ apiKey: foundryApiKey,
129
+ baseUrl: resolveAnthropicBaseUrlFromEnv() ?? DEFAULT_BASE_URL,
130
+ isOAuth: isOAuthToken(foundryApiKey),
131
+ };
132
+ }
133
+ // 3. OAuth credentials in agent.db (with 5-minute expiry buffer)
104
134
  const expiryBuffer = 5 * 60 * 1000; // 5 minutes
105
135
  const now = Date.now();
106
136
  const credentials = await readAnthropicOAuthCredentials(store);
@@ -115,9 +145,9 @@ export async function findAnthropicAuth(store?: AuthCredentialStore): Promise<An
115
145
  }
116
146
  }
117
147
 
118
- // 3. Generic ANTHROPIC_API_KEY fallback
148
+ // 4. Generic ANTHROPIC_API_KEY fallback
119
149
  const apiKey = getEnvApiKey("anthropic");
120
- const baseUrl = $env.ANTHROPIC_BASE_URL;
150
+ const baseUrl = resolveAnthropicBaseUrlFromEnv();
121
151
  if (apiKey) {
122
152
  return {
123
153
  apiKey,
@@ -72,6 +72,8 @@ export { loginKilo } from "./kilo";
72
72
  export { loginKimi, refreshKimiToken } from "./kimi";
73
73
  // LiteLLM (API key)
74
74
  export { loginLiteLLM } from "./litellm";
75
+ // LM Studio (optional API key)
76
+ export { DEFAULT_LOCAL_TOKEN, loginLmStudio } from "./lm-studio";
75
77
  // MiniMax Coding Plan (API key)
76
78
  export { loginMiniMaxCode, loginMiniMaxCodeCn } from "./minimax-code";
77
79
  // Moonshot (API key)
@@ -163,6 +165,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
163
165
  name: "LiteLLM",
164
166
  available: true,
165
167
  },
168
+ {
169
+ id: "lm-studio",
170
+ name: "LM Studio (Local OpenAI-compatible)",
171
+ available: true,
172
+ },
166
173
  {
167
174
  id: "ollama",
168
175
  name: "Ollama (Local OpenAI-compatible)",
@@ -338,6 +345,7 @@ export async function refreshOAuthToken(
338
345
  case "synthetic":
339
346
  case "together":
340
347
  case "litellm":
348
+ case "lm-studio":
341
349
  case "ollama":
342
350
  case "xiaomi":
343
351
  case "zai":
@@ -0,0 +1,40 @@
1
+ /**
2
+ * LM Studio login flow.
3
+ *
4
+ * LM Studio provides an OpenAI-compatible API at a local base URL.
5
+ * It usually runs unauthenticated but can be configured to require a bearer token.
6
+ *
7
+ * This flow stores an API-key-style credential used by `/login` and auth storage.
8
+ */
9
+
10
+ import type { OAuthController, OAuthProvider } from "./types";
11
+
12
+ const PROVIDER_ID: OAuthProvider = "lm-studio";
13
+ const _AUTH_URL = "https://lmstudio.ai/docs/api";
14
+ const _DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:1234/v1";
15
+ export const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
16
+
17
+ /**
18
+ * Login to LM Studio.
19
+ *
20
+ * Opens LM Studio API docs, prompts for an optional token,
21
+ * and returns a stored key value.
22
+ */
23
+ export async function loginLmStudio(options: OAuthController): Promise<string> {
24
+ if (!options.onPrompt) {
25
+ throw new Error(`${PROVIDER_ID} login requires onPrompt callback`);
26
+ }
27
+
28
+ const apiKey = await options.onPrompt({
29
+ message: "Optional: Paste LM Studio API key (to customize endpoint URL, set LM_STUDIO_BASE_URL env var)",
30
+ placeholder: DEFAULT_LOCAL_TOKEN,
31
+ allowEmpty: true,
32
+ });
33
+
34
+ if (options.signal?.aborted) {
35
+ throw new Error("Login cancelled");
36
+ }
37
+
38
+ const trimmed = apiKey.trim();
39
+ return trimmed || DEFAULT_LOCAL_TOKEN;
40
+ }
@@ -21,6 +21,7 @@ export type OAuthProvider =
21
21
  | "kimi-code"
22
22
  | "kilo"
23
23
  | "litellm"
24
+ | "lm-studio"
24
25
  | "minimax-code"
25
26
  | "minimax-code-cn"
26
27
  | "moonshot"