@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 +13 -1
- package/README.md +5 -1
- package/package.json +3 -3
- package/src/auth-storage.ts +6 -0
- package/src/models.json +124 -16
- package/src/provider-models/descriptors.ts +8 -0
- package/src/provider-models/openai-compat.ts +37 -1
- package/src/providers/anthropic.ts +139 -28
- package/src/providers/github-copilot-headers.ts +60 -8
- package/src/providers/google-gemini-cli-usage.ts +1 -1
- package/src/providers/openai-completions.ts +30 -14
- package/src/providers/openai-responses.ts +25 -14
- package/src/stream.ts +29 -10
- package/src/types.ts +5 -1
- package/src/utils/anthropic-auth.ts +37 -7
- package/src/utils/oauth/index.ts +8 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/types.ts +1 -0
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.
|
|
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.
|
|
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.
|
|
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",
|
package/src/auth-storage.ts
CHANGED
|
@@ -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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
22992
|
+
"output": 1,
|
|
22943
22993
|
"cacheRead": 0,
|
|
22944
22994
|
"cacheWrite": 0
|
|
22945
22995
|
},
|
|
22946
22996
|
"contextWindow": 262144,
|
|
22947
|
-
"maxTokens":
|
|
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.
|
|
23602
|
-
"output": 2.
|
|
23603
|
-
"cacheRead": 0.
|
|
23670
|
+
"input": 0.7999999999999999,
|
|
23671
|
+
"output": 2.56,
|
|
23672
|
+
"cacheRead": 0.16,
|
|
23604
23673
|
"cacheWrite": 0
|
|
23605
23674
|
},
|
|
23606
|
-
"contextWindow":
|
|
23607
|
-
"maxTokens":
|
|
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
|
|
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 (!
|
|
495
|
+
if (!baseUrl) return undefined;
|
|
389
496
|
|
|
390
497
|
let serverName: string;
|
|
391
498
|
try {
|
|
392
|
-
serverName = new URL(
|
|
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: `${
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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):
|
|
68
|
+
export function getCopilotInitiatorOverride(headers: Record<string, string> | undefined): CopilotInitiator | undefined {
|
|
54
69
|
if (!headers) return undefined;
|
|
55
70
|
|
|
56
|
-
let override:
|
|
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
|
-
|
|
75
|
-
|
|
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":
|
|
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
|
|
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(
|
|
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: `${
|
|
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
|
|
525
|
+
const copilot = buildCopilotDynamicHeaders({
|
|
516
526
|
messages: context.messages,
|
|
517
527
|
hasImages,
|
|
518
|
-
|
|
528
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
529
|
+
headers,
|
|
519
530
|
});
|
|
520
|
-
Object.assign(headers,
|
|
531
|
+
Object.assign(headers, copilot.headers);
|
|
532
|
+
copilotPremiumRequests = copilot.premiumRequests;
|
|
533
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl;
|
|
521
534
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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: `${
|
|
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
|
|
402
|
+
const copilot = buildCopilotDynamicHeaders({
|
|
398
403
|
messages: context.messages,
|
|
399
404
|
hasImages,
|
|
400
|
-
|
|
405
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
406
|
+
headers,
|
|
401
407
|
});
|
|
402
|
-
Object.assign(headers,
|
|
408
|
+
Object.assign(headers, copilot.headers);
|
|
409
|
+
copilotPremiumRequests = copilot.premiumRequests;
|
|
410
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl;
|
|
403
411
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
//
|
|
81
|
-
anthropic: () =>
|
|
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
|
|
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
|
|
693
|
-
return model.id
|
|
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
|
|
698
|
-
return model.id
|
|
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
|
|
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.
|
|
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
|
|
104
|
+
* Finds Anthropic auth config using priority:
|
|
85
105
|
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
86
|
-
* 2.
|
|
87
|
-
* 3.
|
|
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.
|
|
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
|
-
//
|
|
148
|
+
// 4. Generic ANTHROPIC_API_KEY fallback
|
|
119
149
|
const apiKey = getEnvApiKey("anthropic");
|
|
120
|
-
const baseUrl =
|
|
150
|
+
const baseUrl = resolveAnthropicBaseUrlFromEnv();
|
|
121
151
|
if (apiKey) {
|
|
122
152
|
return {
|
|
123
153
|
apiKey,
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -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
|
+
}
|