@oh-my-pi/pi-ai 14.0.4 → 14.0.5
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 +16 -1
- package/package.json +2 -2
- package/src/auth-storage.ts +8 -2
- package/src/models.json +49 -124
- package/src/provider-models/openai-compat.ts +18 -25
- package/src/providers/anthropic.ts +6 -3
- package/src/providers/github-copilot-headers.ts +5 -3
- package/src/providers/openai-codex-responses.ts +1 -1
- package/src/providers/openai-completions.ts +10 -5
- package/src/providers/openai-responses-shared.ts +1 -1
- package/src/providers/openai-responses.ts +6 -2
- package/src/providers/transform-messages.ts +4 -1
- package/src/usage/github-copilot.ts +2 -8
- package/src/utils/http-inspector.ts +20 -0
- package/src/utils/idle-iterator.ts +1 -1
- package/src/utils/oauth/github-copilot.ts +94 -81
- package/src/utils/oauth/index.ts +6 -4
- package/src/utils.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.0.5] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Replaced GitHub Copilot authentication from VSCode extension impersonation to the opencode OAuth flow, eliminating TOS concerns. Existing users will need to re-authenticate once with `/login github-copilot`.
|
|
9
|
+
- Simplified Copilot token handling: GitHub OAuth token is used directly for all API requests (no JWT exchange or refresh cycle).
|
|
10
|
+
- Changed GitHub Copilot API base URL from `api.individual.githubcopilot.com` to `api.githubcopilot.com`.
|
|
11
|
+
- Updated default OpenAI stream idle timeout to 120,000 milliseconds to keep stream generation alive longer
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed duplicate synthetic tool results being generated when a real tool result appears later in message history
|
|
16
|
+
- Fixed GitHub Copilot `/models` discovery to unwrap structured OAuth credentials before sending the bearer token, preserving dynamic catalog refresh for OAuth-backed callers.
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
- Removed Copilot JWT proxy-ep base URL resolution (no longer needed with opencode auth).
|
|
5
20
|
## [14.0.3] - 2026-04-09
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -1994,4 +2009,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
|
|
|
1994
2009
|
|
|
1995
2010
|
## [0.9.4] - 2025-11-26
|
|
1996
2011
|
|
|
1997
|
-
Initial release with multi-provider LLM support.
|
|
2012
|
+
Initial release with multi-provider LLM support.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "14.0.
|
|
4
|
+
"version": "14.0.5",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@aws-sdk/client-bedrock-runtime": "^3",
|
|
46
46
|
"@bufbuild/protobuf": "^2.11",
|
|
47
47
|
"@google/genai": "^1.43",
|
|
48
|
-
"@oh-my-pi/pi-utils": "14.0.
|
|
48
|
+
"@oh-my-pi/pi-utils": "14.0.5",
|
|
49
49
|
"@sinclair/typebox": "^0.34",
|
|
50
50
|
"@smithy/node-http-handler": "^4.4",
|
|
51
51
|
"ajv": "^8.18",
|
package/src/auth-storage.ts
CHANGED
|
@@ -1882,8 +1882,8 @@ export class AuthStorage {
|
|
|
1882
1882
|
/**
|
|
1883
1883
|
* Peek at API key for a provider without refreshing OAuth tokens.
|
|
1884
1884
|
* Used for model discovery where we only need to know if credentials exist
|
|
1885
|
-
* and get a best-effort token.
|
|
1886
|
-
*
|
|
1885
|
+
* and get a best-effort token. For GitHub Copilot we preserve enterprise
|
|
1886
|
+
* routing metadata so discovery can hit the correct host.
|
|
1887
1887
|
*/
|
|
1888
1888
|
async peekApiKey(provider: string): Promise<string | undefined> {
|
|
1889
1889
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
@@ -1901,6 +1901,12 @@ export class AuthStorage {
|
|
|
1901
1901
|
if (oauthSelection) {
|
|
1902
1902
|
const expiresAt = oauthSelection.credential.expires;
|
|
1903
1903
|
if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
|
|
1904
|
+
if (provider === "github-copilot") {
|
|
1905
|
+
return JSON.stringify({
|
|
1906
|
+
token: oauthSelection.credential.access,
|
|
1907
|
+
enterpriseUrl: oauthSelection.credential.enterpriseUrl,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1904
1910
|
return oauthSelection.credential.access;
|
|
1905
1911
|
}
|
|
1906
1912
|
}
|
package/src/models.json
CHANGED
|
@@ -4582,7 +4582,7 @@
|
|
|
4582
4582
|
"name": "Claude Haiku 4.5",
|
|
4583
4583
|
"api": "anthropic-messages",
|
|
4584
4584
|
"provider": "github-copilot",
|
|
4585
|
-
"baseUrl": "https://api.
|
|
4585
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4586
4586
|
"reasoning": true,
|
|
4587
4587
|
"input": [
|
|
4588
4588
|
"text",
|
|
@@ -4597,10 +4597,7 @@
|
|
|
4597
4597
|
"contextWindow": 144000,
|
|
4598
4598
|
"maxTokens": 32000,
|
|
4599
4599
|
"headers": {
|
|
4600
|
-
"User-Agent": "
|
|
4601
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4602
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4603
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4600
|
+
"User-Agent": "opencode/1.3.15"
|
|
4604
4601
|
},
|
|
4605
4602
|
"premiumMultiplier": 0.33,
|
|
4606
4603
|
"thinking": {
|
|
@@ -4614,7 +4611,7 @@
|
|
|
4614
4611
|
"name": "Claude Opus 4.5",
|
|
4615
4612
|
"api": "anthropic-messages",
|
|
4616
4613
|
"provider": "github-copilot",
|
|
4617
|
-
"baseUrl": "https://api.
|
|
4614
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4618
4615
|
"reasoning": true,
|
|
4619
4616
|
"input": [
|
|
4620
4617
|
"text",
|
|
@@ -4629,10 +4626,7 @@
|
|
|
4629
4626
|
"contextWindow": 160000,
|
|
4630
4627
|
"maxTokens": 32000,
|
|
4631
4628
|
"headers": {
|
|
4632
|
-
"User-Agent": "
|
|
4633
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4634
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4635
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4629
|
+
"User-Agent": "opencode/1.3.15"
|
|
4636
4630
|
},
|
|
4637
4631
|
"thinking": {
|
|
4638
4632
|
"mode": "anthropic-budget-effort",
|
|
@@ -4645,7 +4639,7 @@
|
|
|
4645
4639
|
"name": "Claude Opus 4.6",
|
|
4646
4640
|
"api": "anthropic-messages",
|
|
4647
4641
|
"provider": "github-copilot",
|
|
4648
|
-
"baseUrl": "https://api.
|
|
4642
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4649
4643
|
"reasoning": true,
|
|
4650
4644
|
"input": [
|
|
4651
4645
|
"text",
|
|
@@ -4660,10 +4654,7 @@
|
|
|
4660
4654
|
"contextWindow": 1000000,
|
|
4661
4655
|
"maxTokens": 64000,
|
|
4662
4656
|
"headers": {
|
|
4663
|
-
"User-Agent": "
|
|
4664
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4665
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4666
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4657
|
+
"User-Agent": "opencode/1.3.15"
|
|
4667
4658
|
},
|
|
4668
4659
|
"premiumMultiplier": 3,
|
|
4669
4660
|
"thinking": {
|
|
@@ -4677,7 +4668,7 @@
|
|
|
4677
4668
|
"name": "Claude Sonnet 4",
|
|
4678
4669
|
"api": "anthropic-messages",
|
|
4679
4670
|
"provider": "github-copilot",
|
|
4680
|
-
"baseUrl": "https://api.
|
|
4671
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4681
4672
|
"reasoning": true,
|
|
4682
4673
|
"input": [
|
|
4683
4674
|
"text",
|
|
@@ -4692,10 +4683,7 @@
|
|
|
4692
4683
|
"contextWindow": 216000,
|
|
4693
4684
|
"maxTokens": 16000,
|
|
4694
4685
|
"headers": {
|
|
4695
|
-
"User-Agent": "
|
|
4696
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4697
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4698
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4686
|
+
"User-Agent": "opencode/1.3.15"
|
|
4699
4687
|
},
|
|
4700
4688
|
"thinking": {
|
|
4701
4689
|
"mode": "budget",
|
|
@@ -4708,7 +4696,7 @@
|
|
|
4708
4696
|
"name": "Claude Sonnet 4.5",
|
|
4709
4697
|
"api": "anthropic-messages",
|
|
4710
4698
|
"provider": "github-copilot",
|
|
4711
|
-
"baseUrl": "https://api.
|
|
4699
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4712
4700
|
"reasoning": true,
|
|
4713
4701
|
"input": [
|
|
4714
4702
|
"text",
|
|
@@ -4723,10 +4711,7 @@
|
|
|
4723
4711
|
"contextWindow": 144000,
|
|
4724
4712
|
"maxTokens": 32000,
|
|
4725
4713
|
"headers": {
|
|
4726
|
-
"User-Agent": "
|
|
4727
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4728
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4729
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4714
|
+
"User-Agent": "opencode/1.3.15"
|
|
4730
4715
|
},
|
|
4731
4716
|
"thinking": {
|
|
4732
4717
|
"mode": "anthropic-budget-effort",
|
|
@@ -4739,7 +4724,7 @@
|
|
|
4739
4724
|
"name": "Claude Sonnet 4.6",
|
|
4740
4725
|
"api": "anthropic-messages",
|
|
4741
4726
|
"provider": "github-copilot",
|
|
4742
|
-
"baseUrl": "https://api.
|
|
4727
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4743
4728
|
"reasoning": true,
|
|
4744
4729
|
"input": [
|
|
4745
4730
|
"text",
|
|
@@ -4754,10 +4739,7 @@
|
|
|
4754
4739
|
"contextWindow": 200000,
|
|
4755
4740
|
"maxTokens": 32000,
|
|
4756
4741
|
"headers": {
|
|
4757
|
-
"User-Agent": "
|
|
4758
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4759
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4760
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4742
|
+
"User-Agent": "opencode/1.3.15"
|
|
4761
4743
|
},
|
|
4762
4744
|
"thinking": {
|
|
4763
4745
|
"mode": "anthropic-adaptive",
|
|
@@ -4770,7 +4752,7 @@
|
|
|
4770
4752
|
"name": "Gemini 2.5 Pro",
|
|
4771
4753
|
"api": "openai-completions",
|
|
4772
4754
|
"provider": "github-copilot",
|
|
4773
|
-
"baseUrl": "https://api.
|
|
4755
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4774
4756
|
"reasoning": false,
|
|
4775
4757
|
"input": [
|
|
4776
4758
|
"text",
|
|
@@ -4785,10 +4767,7 @@
|
|
|
4785
4767
|
"contextWindow": 128000,
|
|
4786
4768
|
"maxTokens": 64000,
|
|
4787
4769
|
"headers": {
|
|
4788
|
-
"User-Agent": "
|
|
4789
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4790
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4791
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4770
|
+
"User-Agent": "opencode/1.3.15"
|
|
4792
4771
|
},
|
|
4793
4772
|
"compat": {
|
|
4794
4773
|
"supportsStore": false,
|
|
@@ -4801,7 +4780,7 @@
|
|
|
4801
4780
|
"name": "Gemini 3 Flash",
|
|
4802
4781
|
"api": "openai-completions",
|
|
4803
4782
|
"provider": "github-copilot",
|
|
4804
|
-
"baseUrl": "https://api.
|
|
4783
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4805
4784
|
"reasoning": true,
|
|
4806
4785
|
"input": [
|
|
4807
4786
|
"text",
|
|
@@ -4816,10 +4795,7 @@
|
|
|
4816
4795
|
"contextWindow": 128000,
|
|
4817
4796
|
"maxTokens": 64000,
|
|
4818
4797
|
"headers": {
|
|
4819
|
-
"User-Agent": "
|
|
4820
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4821
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4822
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4798
|
+
"User-Agent": "opencode/1.3.15"
|
|
4823
4799
|
},
|
|
4824
4800
|
"compat": {
|
|
4825
4801
|
"supportsStore": false,
|
|
@@ -4837,7 +4813,7 @@
|
|
|
4837
4813
|
"name": "Gemini 3 Pro Preview",
|
|
4838
4814
|
"api": "openai-completions",
|
|
4839
4815
|
"provider": "github-copilot",
|
|
4840
|
-
"baseUrl": "https://api.
|
|
4816
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4841
4817
|
"reasoning": true,
|
|
4842
4818
|
"input": [
|
|
4843
4819
|
"text",
|
|
@@ -4852,10 +4828,7 @@
|
|
|
4852
4828
|
"contextWindow": 128000,
|
|
4853
4829
|
"maxTokens": 64000,
|
|
4854
4830
|
"headers": {
|
|
4855
|
-
"User-Agent": "
|
|
4856
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4857
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4858
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4831
|
+
"User-Agent": "opencode/1.3.15"
|
|
4859
4832
|
},
|
|
4860
4833
|
"compat": {
|
|
4861
4834
|
"supportsStore": false,
|
|
@@ -4873,7 +4846,7 @@
|
|
|
4873
4846
|
"name": "Gemini 3.1 Pro Preview",
|
|
4874
4847
|
"api": "openai-completions",
|
|
4875
4848
|
"provider": "github-copilot",
|
|
4876
|
-
"baseUrl": "https://api.
|
|
4849
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4877
4850
|
"reasoning": true,
|
|
4878
4851
|
"input": [
|
|
4879
4852
|
"text",
|
|
@@ -4888,10 +4861,7 @@
|
|
|
4888
4861
|
"contextWindow": 128000,
|
|
4889
4862
|
"maxTokens": 64000,
|
|
4890
4863
|
"headers": {
|
|
4891
|
-
"User-Agent": "
|
|
4892
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4893
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4894
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4864
|
+
"User-Agent": "opencode/1.3.15"
|
|
4895
4865
|
},
|
|
4896
4866
|
"compat": {
|
|
4897
4867
|
"supportsStore": false,
|
|
@@ -4909,7 +4879,7 @@
|
|
|
4909
4879
|
"name": "GPT-4.1",
|
|
4910
4880
|
"api": "openai-completions",
|
|
4911
4881
|
"provider": "github-copilot",
|
|
4912
|
-
"baseUrl": "https://api.
|
|
4882
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4913
4883
|
"reasoning": false,
|
|
4914
4884
|
"input": [
|
|
4915
4885
|
"text",
|
|
@@ -4924,10 +4894,7 @@
|
|
|
4924
4894
|
"contextWindow": 128000,
|
|
4925
4895
|
"maxTokens": 16384,
|
|
4926
4896
|
"headers": {
|
|
4927
|
-
"User-Agent": "
|
|
4928
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4929
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4930
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4897
|
+
"User-Agent": "opencode/1.3.15"
|
|
4931
4898
|
},
|
|
4932
4899
|
"compat": {
|
|
4933
4900
|
"supportsStore": false,
|
|
@@ -4940,7 +4907,7 @@
|
|
|
4940
4907
|
"name": "GPT-4o",
|
|
4941
4908
|
"api": "openai-completions",
|
|
4942
4909
|
"provider": "github-copilot",
|
|
4943
|
-
"baseUrl": "https://api.
|
|
4910
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4944
4911
|
"reasoning": false,
|
|
4945
4912
|
"input": [
|
|
4946
4913
|
"text",
|
|
@@ -4955,10 +4922,7 @@
|
|
|
4955
4922
|
"contextWindow": 128000,
|
|
4956
4923
|
"maxTokens": 4096,
|
|
4957
4924
|
"headers": {
|
|
4958
|
-
"User-Agent": "
|
|
4959
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4960
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4961
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4925
|
+
"User-Agent": "opencode/1.3.15"
|
|
4962
4926
|
},
|
|
4963
4927
|
"compat": {
|
|
4964
4928
|
"supportsStore": false,
|
|
@@ -4972,7 +4936,7 @@
|
|
|
4972
4936
|
"name": "GPT-5",
|
|
4973
4937
|
"api": "openai-responses",
|
|
4974
4938
|
"provider": "github-copilot",
|
|
4975
|
-
"baseUrl": "https://api.
|
|
4939
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
4976
4940
|
"reasoning": true,
|
|
4977
4941
|
"input": [
|
|
4978
4942
|
"text",
|
|
@@ -4987,10 +4951,7 @@
|
|
|
4987
4951
|
"contextWindow": 128000,
|
|
4988
4952
|
"maxTokens": 128000,
|
|
4989
4953
|
"headers": {
|
|
4990
|
-
"User-Agent": "
|
|
4991
|
-
"Editor-Version": "vscode/1.107.0",
|
|
4992
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
4993
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4954
|
+
"User-Agent": "opencode/1.3.15"
|
|
4994
4955
|
},
|
|
4995
4956
|
"thinking": {
|
|
4996
4957
|
"mode": "effort",
|
|
@@ -5003,7 +4964,7 @@
|
|
|
5003
4964
|
"name": "GPT-5-mini",
|
|
5004
4965
|
"api": "openai-responses",
|
|
5005
4966
|
"provider": "github-copilot",
|
|
5006
|
-
"baseUrl": "https://api.
|
|
4967
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5007
4968
|
"reasoning": true,
|
|
5008
4969
|
"input": [
|
|
5009
4970
|
"text",
|
|
@@ -5018,10 +4979,7 @@
|
|
|
5018
4979
|
"contextWindow": 264000,
|
|
5019
4980
|
"maxTokens": 64000,
|
|
5020
4981
|
"headers": {
|
|
5021
|
-
"User-Agent": "
|
|
5022
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5023
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5024
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
4982
|
+
"User-Agent": "opencode/1.3.15"
|
|
5025
4983
|
},
|
|
5026
4984
|
"thinking": {
|
|
5027
4985
|
"mode": "effort",
|
|
@@ -5034,7 +4992,7 @@
|
|
|
5034
4992
|
"name": "GPT-5.1",
|
|
5035
4993
|
"api": "openai-responses",
|
|
5036
4994
|
"provider": "github-copilot",
|
|
5037
|
-
"baseUrl": "https://api.
|
|
4995
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5038
4996
|
"reasoning": true,
|
|
5039
4997
|
"input": [
|
|
5040
4998
|
"text",
|
|
@@ -5049,10 +5007,7 @@
|
|
|
5049
5007
|
"contextWindow": 264000,
|
|
5050
5008
|
"maxTokens": 64000,
|
|
5051
5009
|
"headers": {
|
|
5052
|
-
"User-Agent": "
|
|
5053
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5054
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5055
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5010
|
+
"User-Agent": "opencode/1.3.15"
|
|
5056
5011
|
},
|
|
5057
5012
|
"thinking": {
|
|
5058
5013
|
"mode": "effort",
|
|
@@ -5065,7 +5020,7 @@
|
|
|
5065
5020
|
"name": "GPT-5.1-Codex",
|
|
5066
5021
|
"api": "openai-responses",
|
|
5067
5022
|
"provider": "github-copilot",
|
|
5068
|
-
"baseUrl": "https://api.
|
|
5023
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5069
5024
|
"reasoning": true,
|
|
5070
5025
|
"input": [
|
|
5071
5026
|
"text",
|
|
@@ -5080,10 +5035,7 @@
|
|
|
5080
5035
|
"contextWindow": 272000,
|
|
5081
5036
|
"maxTokens": 128000,
|
|
5082
5037
|
"headers": {
|
|
5083
|
-
"User-Agent": "
|
|
5084
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5085
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5086
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5038
|
+
"User-Agent": "opencode/1.3.15"
|
|
5087
5039
|
},
|
|
5088
5040
|
"thinking": {
|
|
5089
5041
|
"mode": "effort",
|
|
@@ -5096,7 +5048,7 @@
|
|
|
5096
5048
|
"name": "GPT-5.1-Codex-max",
|
|
5097
5049
|
"api": "openai-responses",
|
|
5098
5050
|
"provider": "github-copilot",
|
|
5099
|
-
"baseUrl": "https://api.
|
|
5051
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5100
5052
|
"reasoning": true,
|
|
5101
5053
|
"input": [
|
|
5102
5054
|
"text",
|
|
@@ -5111,10 +5063,7 @@
|
|
|
5111
5063
|
"contextWindow": 272000,
|
|
5112
5064
|
"maxTokens": 128000,
|
|
5113
5065
|
"headers": {
|
|
5114
|
-
"User-Agent": "
|
|
5115
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5116
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5117
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5066
|
+
"User-Agent": "opencode/1.3.15"
|
|
5118
5067
|
},
|
|
5119
5068
|
"thinking": {
|
|
5120
5069
|
"mode": "effort",
|
|
@@ -5127,7 +5076,7 @@
|
|
|
5127
5076
|
"name": "GPT-5.1-Codex-mini",
|
|
5128
5077
|
"api": "openai-responses",
|
|
5129
5078
|
"provider": "github-copilot",
|
|
5130
|
-
"baseUrl": "https://api.
|
|
5079
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5131
5080
|
"reasoning": true,
|
|
5132
5081
|
"input": [
|
|
5133
5082
|
"text",
|
|
@@ -5142,10 +5091,7 @@
|
|
|
5142
5091
|
"contextWindow": 272000,
|
|
5143
5092
|
"maxTokens": 128000,
|
|
5144
5093
|
"headers": {
|
|
5145
|
-
"User-Agent": "
|
|
5146
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5147
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5148
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5094
|
+
"User-Agent": "opencode/1.3.15"
|
|
5149
5095
|
},
|
|
5150
5096
|
"thinking": {
|
|
5151
5097
|
"mode": "effort",
|
|
@@ -5158,7 +5104,7 @@
|
|
|
5158
5104
|
"name": "GPT-5.2",
|
|
5159
5105
|
"api": "openai-responses",
|
|
5160
5106
|
"provider": "github-copilot",
|
|
5161
|
-
"baseUrl": "https://api.
|
|
5107
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5162
5108
|
"reasoning": true,
|
|
5163
5109
|
"input": [
|
|
5164
5110
|
"text",
|
|
@@ -5173,10 +5119,7 @@
|
|
|
5173
5119
|
"contextWindow": 264000,
|
|
5174
5120
|
"maxTokens": 64000,
|
|
5175
5121
|
"headers": {
|
|
5176
|
-
"User-Agent": "
|
|
5177
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5178
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5179
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5122
|
+
"User-Agent": "opencode/1.3.15"
|
|
5180
5123
|
},
|
|
5181
5124
|
"thinking": {
|
|
5182
5125
|
"mode": "effort",
|
|
@@ -5189,7 +5132,7 @@
|
|
|
5189
5132
|
"name": "GPT-5.2-Codex",
|
|
5190
5133
|
"api": "openai-responses",
|
|
5191
5134
|
"provider": "github-copilot",
|
|
5192
|
-
"baseUrl": "https://api.
|
|
5135
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5193
5136
|
"reasoning": true,
|
|
5194
5137
|
"input": [
|
|
5195
5138
|
"text",
|
|
@@ -5204,10 +5147,7 @@
|
|
|
5204
5147
|
"contextWindow": 272000,
|
|
5205
5148
|
"maxTokens": 128000,
|
|
5206
5149
|
"headers": {
|
|
5207
|
-
"User-Agent": "
|
|
5208
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5209
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5210
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5150
|
+
"User-Agent": "opencode/1.3.15"
|
|
5211
5151
|
},
|
|
5212
5152
|
"thinking": {
|
|
5213
5153
|
"mode": "effort",
|
|
@@ -5220,7 +5160,7 @@
|
|
|
5220
5160
|
"name": "GPT-5.3-Codex",
|
|
5221
5161
|
"api": "openai-responses",
|
|
5222
5162
|
"provider": "github-copilot",
|
|
5223
|
-
"baseUrl": "https://api.
|
|
5163
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5224
5164
|
"reasoning": true,
|
|
5225
5165
|
"input": [
|
|
5226
5166
|
"text",
|
|
@@ -5235,10 +5175,7 @@
|
|
|
5235
5175
|
"contextWindow": 272000,
|
|
5236
5176
|
"maxTokens": 128000,
|
|
5237
5177
|
"headers": {
|
|
5238
|
-
"User-Agent": "
|
|
5239
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5240
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5241
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5178
|
+
"User-Agent": "opencode/1.3.15"
|
|
5242
5179
|
},
|
|
5243
5180
|
"thinking": {
|
|
5244
5181
|
"mode": "effort",
|
|
@@ -5251,7 +5188,7 @@
|
|
|
5251
5188
|
"name": "GPT-5.4",
|
|
5252
5189
|
"api": "openai-responses",
|
|
5253
5190
|
"provider": "github-copilot",
|
|
5254
|
-
"baseUrl": "https://api.
|
|
5191
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5255
5192
|
"reasoning": true,
|
|
5256
5193
|
"input": [
|
|
5257
5194
|
"text",
|
|
@@ -5266,10 +5203,7 @@
|
|
|
5266
5203
|
"contextWindow": 400000,
|
|
5267
5204
|
"maxTokens": 128000,
|
|
5268
5205
|
"headers": {
|
|
5269
|
-
"User-Agent": "
|
|
5270
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5271
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5272
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5206
|
+
"User-Agent": "opencode/1.3.15"
|
|
5273
5207
|
},
|
|
5274
5208
|
"thinking": {
|
|
5275
5209
|
"mode": "effort",
|
|
@@ -5282,7 +5216,7 @@
|
|
|
5282
5216
|
"name": "GPT-5.4 Mini",
|
|
5283
5217
|
"api": "openai-responses",
|
|
5284
5218
|
"provider": "github-copilot",
|
|
5285
|
-
"baseUrl": "https://api.
|
|
5219
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5286
5220
|
"reasoning": true,
|
|
5287
5221
|
"input": [
|
|
5288
5222
|
"text",
|
|
@@ -5297,10 +5231,7 @@
|
|
|
5297
5231
|
"contextWindow": 400000,
|
|
5298
5232
|
"maxTokens": 128000,
|
|
5299
5233
|
"headers": {
|
|
5300
|
-
"User-Agent": "
|
|
5301
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5302
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5303
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5234
|
+
"User-Agent": "opencode/1.3.15"
|
|
5304
5235
|
},
|
|
5305
5236
|
"premiumMultiplier": 0.33,
|
|
5306
5237
|
"thinking": {
|
|
@@ -5314,7 +5245,7 @@
|
|
|
5314
5245
|
"name": "Grok Code Fast 1",
|
|
5315
5246
|
"api": "openai-completions",
|
|
5316
5247
|
"provider": "github-copilot",
|
|
5317
|
-
"baseUrl": "https://api.
|
|
5248
|
+
"baseUrl": "https://api.githubcopilot.com",
|
|
5318
5249
|
"reasoning": true,
|
|
5319
5250
|
"input": [
|
|
5320
5251
|
"text"
|
|
@@ -5328,10 +5259,7 @@
|
|
|
5328
5259
|
"contextWindow": 128000,
|
|
5329
5260
|
"maxTokens": 64000,
|
|
5330
5261
|
"headers": {
|
|
5331
|
-
"User-Agent": "
|
|
5332
|
-
"Editor-Version": "vscode/1.107.0",
|
|
5333
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5334
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
5262
|
+
"User-Agent": "opencode/1.3.15"
|
|
5335
5263
|
},
|
|
5336
5264
|
"compat": {
|
|
5337
5265
|
"supportsStore": false,
|
|
@@ -21292,10 +21220,7 @@
|
|
|
21292
21220
|
"contextWindow": 1048576,
|
|
21293
21221
|
"maxTokens": 65536,
|
|
21294
21222
|
"headers": {
|
|
21295
|
-
"User-Agent": "
|
|
21296
|
-
"Editor-Version": "vscode/1.107.0",
|
|
21297
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
21298
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
21223
|
+
"User-Agent": "opencode/1.3.15"
|
|
21299
21224
|
},
|
|
21300
21225
|
"compat": {
|
|
21301
21226
|
"supportsStore": false,
|
|
@@ -7,7 +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
|
+
import { getGitHubCopilotBaseUrl, OPENCODE_HEADERS, parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
11
11
|
|
|
12
12
|
const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
13
13
|
const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
@@ -661,6 +661,9 @@ export function openrouterModelManagerOptions(
|
|
|
661
661
|
provider: "openrouter",
|
|
662
662
|
baseUrl,
|
|
663
663
|
apiKey,
|
|
664
|
+
headers: {
|
|
665
|
+
"X-Title": "Oh-My-Pi",
|
|
666
|
+
},
|
|
664
667
|
filterModel: (entry: OpenAICompatibleModelRecord) => {
|
|
665
668
|
const params = entry.supported_parameters;
|
|
666
669
|
return Array.isArray(params) && params.includes("tools");
|
|
@@ -1438,12 +1441,6 @@ export interface GithubCopilotModelManagerConfig {
|
|
|
1438
1441
|
apiKey?: string;
|
|
1439
1442
|
baseUrl?: string;
|
|
1440
1443
|
}
|
|
1441
|
-
const GITHUB_COPILOT_HEADERS: Record<string, string> = {
|
|
1442
|
-
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
1443
|
-
"Editor-Version": "vscode/1.107.0",
|
|
1444
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
1445
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
1446
|
-
};
|
|
1447
1444
|
|
|
1448
1445
|
function inferCopilotApi(modelId: string): Api {
|
|
1449
1446
|
if (/^claude-(haiku|sonnet|opus)-4([.-]|$)/.test(modelId)) {
|
|
@@ -1477,12 +1474,14 @@ function extractCopilotLimits(entry: OpenAICompatibleModelRecord): {
|
|
|
1477
1474
|
}
|
|
1478
1475
|
|
|
1479
1476
|
export function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1477
|
+
const rawApiKey = config?.apiKey;
|
|
1478
|
+
const baseUrl = config?.baseUrl ?? "https://api.githubcopilot.com";
|
|
1479
|
+
const parsedApiKey = rawApiKey ? parseGitHubCopilotApiKey(rawApiKey) : undefined;
|
|
1480
|
+
const apiKey = parsedApiKey?.accessToken;
|
|
1481
|
+
const resolvedBaseUrl =
|
|
1482
|
+
parsedApiKey?.enterpriseUrl && baseUrl.includes("githubcopilot.com")
|
|
1483
|
+
? getGitHubCopilotBaseUrl(parsedApiKey.enterpriseUrl)
|
|
1484
|
+
: baseUrl;
|
|
1486
1485
|
const references = createBundledReferenceMap<Api>("github-copilot");
|
|
1487
1486
|
const globalReferences = createGlobalReferenceMap();
|
|
1488
1487
|
return {
|
|
@@ -1492,9 +1491,9 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
|
|
|
1492
1491
|
fetchOpenAICompatibleModels<Api>({
|
|
1493
1492
|
api: "openai-completions",
|
|
1494
1493
|
provider: "github-copilot",
|
|
1495
|
-
baseUrl,
|
|
1494
|
+
baseUrl: resolvedBaseUrl,
|
|
1496
1495
|
apiKey,
|
|
1497
|
-
headers:
|
|
1496
|
+
headers: OPENCODE_HEADERS,
|
|
1498
1497
|
mapModel: (
|
|
1499
1498
|
entry: OpenAICompatibleModelRecord,
|
|
1500
1499
|
defaults: Model<Api>,
|
|
@@ -1546,7 +1545,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
|
|
|
1546
1545
|
name,
|
|
1547
1546
|
contextWindow,
|
|
1548
1547
|
maxTokens,
|
|
1549
|
-
headers: { ...
|
|
1548
|
+
headers: { ...OPENCODE_HEADERS, ...(providerReference?.headers ?? {}) },
|
|
1550
1549
|
...(api === "openai-completions"
|
|
1551
1550
|
? {
|
|
1552
1551
|
compat: {
|
|
@@ -1565,7 +1564,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
|
|
|
1565
1564
|
name,
|
|
1566
1565
|
contextWindow,
|
|
1567
1566
|
maxTokens,
|
|
1568
|
-
headers: { ...
|
|
1567
|
+
headers: { ...OPENCODE_HEADERS },
|
|
1569
1568
|
...(api === "openai-completions"
|
|
1570
1569
|
? {
|
|
1571
1570
|
compat: {
|
|
@@ -1778,12 +1777,6 @@ function bedrockCrossRegionId(id: string): string {
|
|
|
1778
1777
|
return id;
|
|
1779
1778
|
}
|
|
1780
1779
|
|
|
1781
|
-
const COPILOT_HEADERS = {
|
|
1782
|
-
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
1783
|
-
"Editor-Version": "vscode/1.107.0",
|
|
1784
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
1785
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
1786
|
-
} as const;
|
|
1787
1780
|
interface ApiResolutionRule {
|
|
1788
1781
|
matches: (modelId: string, raw: ModelsDevModel) => boolean;
|
|
1789
1782
|
resolved: { api: Api; baseUrl: string };
|
|
@@ -1828,7 +1821,7 @@ function createOpenCodeApiResolution(basePath: string): {
|
|
|
1828
1821
|
const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen");
|
|
1829
1822
|
const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go");
|
|
1830
1823
|
|
|
1831
|
-
const COPILOT_BASE_URL = "https://api.
|
|
1824
|
+
const COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
1832
1825
|
|
|
1833
1826
|
const COPILOT_DEFAULT_RESOLUTION = {
|
|
1834
1827
|
api: "openai-completions",
|
|
@@ -2036,7 +2029,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
|
|
|
2036
2029
|
openAiCompletionsDescriptor("github-copilot", "github-copilot", COPILOT_BASE_URL, {
|
|
2037
2030
|
defaultContextWindow: 128000,
|
|
2038
2031
|
defaultMaxTokens: 8192,
|
|
2039
|
-
headers: { ...
|
|
2032
|
+
headers: { ...OPENCODE_HEADERS },
|
|
2040
2033
|
filterModel: (_id, m) => {
|
|
2041
2034
|
if (m.tool_call !== true) return false;
|
|
2042
2035
|
if (m.status === "deprecated") return false;
|
|
@@ -34,9 +34,10 @@ import type {
|
|
|
34
34
|
import { isAnthropicOAuthToken, normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
35
35
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
36
36
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
37
|
-
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
37
|
+
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotAuthError } from "../utils/http-inspector";
|
|
38
38
|
import { createFirstEventWatchdog, getStreamFirstEventTimeoutMs, markFirstStreamEvent } from "../utils/idle-iterator";
|
|
39
39
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
40
|
+
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
40
41
|
import {
|
|
41
42
|
buildCopilotDynamicHeaders,
|
|
42
43
|
hasCopilotVisionInput,
|
|
@@ -978,6 +979,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
978
979
|
const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
|
|
979
980
|
output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
980
981
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
982
|
+
output.errorMessage = rewriteCopilotAuthError(output.errorMessage, error, model.provider);
|
|
981
983
|
output.duration = Date.now() - startTime;
|
|
982
984
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
983
985
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
@@ -1065,6 +1067,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1065
1067
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
1066
1068
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
1067
1069
|
if (model.provider === "github-copilot") {
|
|
1070
|
+
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
1068
1071
|
const betaFeatures = [...extraBetas];
|
|
1069
1072
|
if (interleavedThinking) {
|
|
1070
1073
|
betaFeatures.push("interleaved-thinking-2025-05-14");
|
|
@@ -1073,7 +1076,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1073
1076
|
{
|
|
1074
1077
|
Accept: stream ? "text/event-stream" : "application/json",
|
|
1075
1078
|
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
1076
|
-
Authorization: `Bearer ${
|
|
1079
|
+
Authorization: `Bearer ${copilotApiKey}`,
|
|
1077
1080
|
...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
|
|
1078
1081
|
},
|
|
1079
1082
|
model.headers,
|
|
@@ -1084,7 +1087,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1084
1087
|
return {
|
|
1085
1088
|
isOAuthToken: false,
|
|
1086
1089
|
apiKey: null,
|
|
1087
|
-
authToken:
|
|
1090
|
+
authToken: copilotApiKey,
|
|
1088
1091
|
baseURL: baseUrl,
|
|
1089
1092
|
maxRetries: 5,
|
|
1090
1093
|
dangerouslyAllowBrowser: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "../types";
|
|
2
|
-
import { getGitHubCopilotBaseUrl } from "../utils/oauth/github-copilot";
|
|
2
|
+
import { getGitHubCopilotBaseUrl, parseGitHubCopilotApiKey } 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.
|
|
@@ -15,9 +15,11 @@ export function resolveGitHubCopilotBaseUrl(
|
|
|
15
15
|
baseUrl: string | undefined,
|
|
16
16
|
apiKey: string | undefined,
|
|
17
17
|
): string | undefined {
|
|
18
|
-
if (!apiKey
|
|
18
|
+
if (!apiKey) return baseUrl;
|
|
19
|
+
const { enterpriseUrl } = parseGitHubCopilotApiKey(apiKey);
|
|
20
|
+
if (!enterpriseUrl) return baseUrl;
|
|
19
21
|
if (baseUrl && !baseUrl.includes("githubcopilot.com")) return baseUrl;
|
|
20
|
-
return getGitHubCopilotBaseUrl(
|
|
22
|
+
return getGitHubCopilotBaseUrl(enterpriseUrl);
|
|
21
23
|
}
|
|
22
24
|
export function inferCopilotInitiator(messages: unknown[]): CopilotInitiator {
|
|
23
25
|
if (messages.length === 0) return "user";
|
|
@@ -2134,7 +2134,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2134
2134
|
if (!msgId) {
|
|
2135
2135
|
msgId = `msg_${msgIndex}`;
|
|
2136
2136
|
} else if (msgId.length > 64) {
|
|
2137
|
-
msgId = `msg_${Bun.hash
|
|
2137
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
2138
2138
|
}
|
|
2139
2139
|
outputItems.push({
|
|
2140
2140
|
type: "message",
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "../types";
|
|
32
32
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
33
33
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
34
|
-
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
34
|
+
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotAuthError } from "../utils/http-inspector";
|
|
35
35
|
import {
|
|
36
36
|
createFirstEventWatchdog,
|
|
37
37
|
getOpenAIStreamIdleTimeoutMs,
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
markFirstStreamEvent,
|
|
41
41
|
} from "../utils/idle-iterator";
|
|
42
42
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
43
|
+
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
43
44
|
import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
44
45
|
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
45
46
|
import { mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
|
|
@@ -516,6 +517,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
516
517
|
// Some providers via OpenRouter include extra details here.
|
|
517
518
|
const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
|
|
518
519
|
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
520
|
+
output.errorMessage = rewriteCopilotAuthError(output.errorMessage, error, model.provider);
|
|
519
521
|
output.duration = Date.now() - startTime;
|
|
520
522
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
521
523
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
@@ -545,8 +547,12 @@ async function createClient(
|
|
|
545
547
|
}
|
|
546
548
|
apiKey = $env.OPENAI_API_KEY;
|
|
547
549
|
}
|
|
550
|
+
const rawApiKey = apiKey;
|
|
548
551
|
|
|
549
552
|
let headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
|
|
553
|
+
if (model.provider === "openrouter") {
|
|
554
|
+
headers["X-Title"] = "Oh-My-Pi";
|
|
555
|
+
}
|
|
550
556
|
if (model.provider === "kimi-code") {
|
|
551
557
|
headers = { ...(await getKimiCommonHeaders()), ...headers };
|
|
552
558
|
}
|
|
@@ -554,6 +560,7 @@ async function createClient(
|
|
|
554
560
|
|
|
555
561
|
let baseUrl = model.baseUrl;
|
|
556
562
|
if (model.provider === "github-copilot") {
|
|
563
|
+
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
557
564
|
const hasImages = hasCopilotVisionInput(context.messages);
|
|
558
565
|
const copilot = buildCopilotDynamicHeaders({
|
|
559
566
|
messages: context.messages,
|
|
@@ -564,7 +571,7 @@ async function createClient(
|
|
|
564
571
|
});
|
|
565
572
|
Object.assign(headers, copilot.headers);
|
|
566
573
|
copilotPremiumRequests = copilot.premiumRequests;
|
|
567
|
-
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl,
|
|
574
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
|
|
568
575
|
}
|
|
569
576
|
return {
|
|
570
577
|
client: new OpenAI({
|
|
@@ -805,9 +812,7 @@ export function convertMessages(
|
|
|
805
812
|
|
|
806
813
|
const generateFallbackToolCallId = (seed: string): string => {
|
|
807
814
|
generatedToolCallIdCounter += 1;
|
|
808
|
-
const hash = Bun.hash
|
|
809
|
-
.xxHash64(`${model.provider}:${model.id}:${seed}:${generatedToolCallIdCounter}`)
|
|
810
|
-
.toString(36);
|
|
815
|
+
const hash = Bun.hash(`${model.provider}:${model.id}:${seed}:${generatedToolCallIdCounter}`).toString(36);
|
|
811
816
|
return `call_${hash}`;
|
|
812
817
|
};
|
|
813
818
|
|
|
@@ -144,7 +144,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
144
144
|
if (!msgId) {
|
|
145
145
|
msgId = `msg_${msgIndex}`;
|
|
146
146
|
} else if (msgId.length > 64) {
|
|
147
|
-
msgId = `msg_${Bun.hash
|
|
147
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
148
148
|
}
|
|
149
149
|
outputItems.push({
|
|
150
150
|
type: "message",
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../utils";
|
|
31
31
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
32
32
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
|
-
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
33
|
+
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotAuthError } from "../utils/http-inspector";
|
|
34
34
|
import {
|
|
35
35
|
createFirstEventWatchdog,
|
|
36
36
|
getOpenAIStreamIdleTimeoutMs,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
iterateWithIdleTimeout,
|
|
39
39
|
markFirstStreamEvent,
|
|
40
40
|
} from "../utils/idle-iterator";
|
|
41
|
+
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
41
42
|
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
42
43
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
43
44
|
import {
|
|
@@ -243,6 +244,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
243
244
|
const firstEventTimeoutError = abortTracker.getLocalAbortReason();
|
|
244
245
|
output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
245
246
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
247
|
+
output.errorMessage = rewriteCopilotAuthError(output.errorMessage, error, model.provider);
|
|
246
248
|
output.duration = Date.now() - startTime;
|
|
247
249
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
248
250
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
@@ -272,12 +274,14 @@ function createClient(
|
|
|
272
274
|
}
|
|
273
275
|
apiKey = $env.OPENAI_API_KEY;
|
|
274
276
|
}
|
|
277
|
+
const rawApiKey = apiKey;
|
|
275
278
|
|
|
276
279
|
const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
|
|
277
280
|
let copilotPremiumRequests: number | undefined;
|
|
278
281
|
|
|
279
282
|
let baseUrl = model.baseUrl;
|
|
280
283
|
if (model.provider === "github-copilot") {
|
|
284
|
+
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
281
285
|
const hasImages = hasCopilotVisionInput(context.messages);
|
|
282
286
|
const copilot = buildCopilotDynamicHeaders({
|
|
283
287
|
messages: context.messages,
|
|
@@ -288,7 +292,7 @@ function createClient(
|
|
|
288
292
|
});
|
|
289
293
|
Object.assign(headers, copilot.headers);
|
|
290
294
|
copilotPremiumRequests = copilot.premiumRequests;
|
|
291
|
-
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl,
|
|
295
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
|
|
292
296
|
}
|
|
293
297
|
return {
|
|
294
298
|
client: new OpenAI({
|
|
@@ -122,6 +122,9 @@ export function transformMessages<TApi extends Api>(
|
|
|
122
122
|
}
|
|
123
123
|
return msg;
|
|
124
124
|
});
|
|
125
|
+
const realToolResultIds = new Set(
|
|
126
|
+
transformed.filter((msg): msg is ToolResultMessage => msg.role === "toolResult").map(msg => msg.toolCallId),
|
|
127
|
+
);
|
|
125
128
|
|
|
126
129
|
// Second pass: insert synthetic empty tool results for orphaned tool calls
|
|
127
130
|
// and preserve aborted/errored tool results when they were already persisted.
|
|
@@ -135,7 +138,7 @@ export function transformMessages<TApi extends Api>(
|
|
|
135
138
|
const flushPendingToolCalls = (timestamp: number): void => {
|
|
136
139
|
if (pendingToolCalls.length === 0) return;
|
|
137
140
|
for (const tc of pendingToolCalls) {
|
|
138
|
-
if (!toolCallStatus.has(tc.id)) {
|
|
141
|
+
if (!toolCallStatus.has(tc.id) && !realToolResultIds.has(tc.id)) {
|
|
139
142
|
result.push({
|
|
140
143
|
role: "toolResult",
|
|
141
144
|
toolCallId: tc.id,
|
|
@@ -14,13 +14,7 @@ import type {
|
|
|
14
14
|
UsageWindow,
|
|
15
15
|
} from "../usage";
|
|
16
16
|
import { isRecord, toBoolean, toNumber } from "../utils";
|
|
17
|
-
|
|
18
|
-
const COPILOT_HEADERS = {
|
|
19
|
-
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
20
|
-
"Editor-Version": "vscode/1.107.0",
|
|
21
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
22
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
23
|
-
} as const;
|
|
17
|
+
import { OPENCODE_HEADERS } from "../utils/oauth/github-copilot";
|
|
24
18
|
|
|
25
19
|
type CopilotQuotaDetail = {
|
|
26
20
|
entitlement: number;
|
|
@@ -183,7 +177,7 @@ async function fetchInternalUsage(
|
|
|
183
177
|
"Content-Type": "application/json",
|
|
184
178
|
Accept: "application/json",
|
|
185
179
|
Authorization: `Bearer ${token}`,
|
|
186
|
-
...
|
|
180
|
+
...OPENCODE_HEADERS,
|
|
187
181
|
};
|
|
188
182
|
const data = await fetchJson(ctx, `${githubApiBaseUrl}/copilot_internal/user`, { headers, signal });
|
|
189
183
|
if (!isRecord(data)) throw new Error("Invalid Copilot usage response");
|
|
@@ -54,6 +54,26 @@ export function withHttpStatus(error: unknown, status: number): Error {
|
|
|
54
54
|
return wrapped;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Rewrite error message for GitHub Copilot request failures.
|
|
59
|
+
* Must run AFTER finalizeErrorMessage since it replaces the message entirely.
|
|
60
|
+
*
|
|
61
|
+
* 401 = token invalid/expired → credential removal is safe, prompt re-login.
|
|
62
|
+
* 403 = token valid but access denied (plan, model policy, org restriction) →
|
|
63
|
+
* do NOT reuse the auth-failed string (which triggers credential removal).
|
|
64
|
+
*/
|
|
65
|
+
export function rewriteCopilotAuthError(errorMessage: string, error: unknown, provider: string): string {
|
|
66
|
+
if (provider !== "github-copilot") return errorMessage;
|
|
67
|
+
const status = extractHttpStatusFromError(error);
|
|
68
|
+
if (status === 401) {
|
|
69
|
+
return `GitHub Copilot authentication failed (HTTP 401). Your token may have been revoked. Please re-login with /login github-copilot`;
|
|
70
|
+
}
|
|
71
|
+
if (status === 403) {
|
|
72
|
+
return `GitHub Copilot access denied (HTTP 403). Your account may not have access to this model or feature. Check your Copilot plan or model policy settings.`;
|
|
73
|
+
}
|
|
74
|
+
return errorMessage;
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
function sanitizeDump(dump: RawHttpRequestDump): RawHttpRequestDump {
|
|
58
78
|
return {
|
|
59
79
|
...dump,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
const DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS =
|
|
3
|
+
const DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
4
|
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 60_000;
|
|
5
5
|
|
|
6
6
|
function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GitHub Copilot OAuth flow
|
|
2
|
+
* GitHub Copilot OAuth flow (opencode OAuth app)
|
|
3
3
|
*/
|
|
4
4
|
import { abortableSleep } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { getBundledModels } from "../../models";
|
|
6
6
|
import type { OAuthCredentials } from "./types";
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
|
|
8
|
+
const CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
10
|
+
export const COPILOT_USER_AGENT = "opencode/1.3.15" as const;
|
|
11
|
+
|
|
12
|
+
export const OPENCODE_HEADERS = {
|
|
13
|
+
"User-Agent": COPILOT_USER_AGENT,
|
|
16
14
|
} as const;
|
|
17
15
|
|
|
18
16
|
const INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2;
|
|
@@ -37,6 +35,47 @@ type DeviceTokenErrorResponse = {
|
|
|
37
35
|
interval?: number;
|
|
38
36
|
};
|
|
39
37
|
|
|
38
|
+
type GitHubCopilotApiKeyPayload = {
|
|
39
|
+
token?: unknown;
|
|
40
|
+
enterpriseUrl?: unknown;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ParsedGitHubCopilotApiKey = {
|
|
44
|
+
accessToken: string;
|
|
45
|
+
enterpriseUrl?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PUBLIC_GITHUB_HOSTS = new Set(["api.github.com", "github.com", "www.github.com"]);
|
|
49
|
+
|
|
50
|
+
function isPublicGitHubHost(host: string): boolean {
|
|
51
|
+
return PUBLIC_GITHUB_HOSTS.has(host.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeGitHubCopilotEnterpriseDomain(input: string | undefined): string | undefined {
|
|
55
|
+
const trimmed = input?.trim();
|
|
56
|
+
if (!trimmed) return undefined;
|
|
57
|
+
const normalized = normalizeDomain(trimmed) ?? trimmed.toLowerCase();
|
|
58
|
+
if (!normalized || isPublicGitHubHost(normalized)) return undefined;
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseGitHubCopilotApiKey(apiKeyRaw: string): ParsedGitHubCopilotApiKey {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(apiKeyRaw) as GitHubCopilotApiKeyPayload;
|
|
65
|
+
if (typeof parsed.token === "string") {
|
|
66
|
+
return {
|
|
67
|
+
accessToken: parsed.token,
|
|
68
|
+
enterpriseUrl:
|
|
69
|
+
typeof parsed.enterpriseUrl === "string"
|
|
70
|
+
? normalizeGitHubCopilotEnterpriseDomain(parsed.enterpriseUrl)
|
|
71
|
+
: undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
76
|
+
return { accessToken: apiKeyRaw };
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
export function normalizeDomain(input: string): string | null {
|
|
41
80
|
const trimmed = input.trim();
|
|
42
81
|
if (!trimmed) return null;
|
|
@@ -51,38 +90,20 @@ export function normalizeDomain(input: string): string | null {
|
|
|
51
90
|
function getUrls(domain: string): {
|
|
52
91
|
deviceCodeUrl: string;
|
|
53
92
|
accessTokenUrl: string;
|
|
54
|
-
copilotTokenUrl: string;
|
|
55
93
|
} {
|
|
56
94
|
return {
|
|
57
95
|
deviceCodeUrl: `https://${domain}/login/device/code`,
|
|
58
96
|
accessTokenUrl: `https://${domain}/login/oauth/access_token`,
|
|
59
|
-
copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,
|
|
60
97
|
};
|
|
61
98
|
}
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!match) return null;
|
|
71
|
-
const proxyHost = match[1];
|
|
72
|
-
// Convert proxy.xxx to api.xxx
|
|
73
|
-
const apiHost = proxyHost.replace(/^proxy\./, "api.");
|
|
74
|
-
return `https://${apiHost}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {
|
|
78
|
-
// If we have a token, extract the base URL from proxy-ep
|
|
79
|
-
if (token) {
|
|
80
|
-
const urlFromToken = getBaseUrlFromToken(token);
|
|
81
|
-
if (urlFromToken) return urlFromToken;
|
|
82
|
-
}
|
|
83
|
-
// Fallback for enterprise or if token parsing fails
|
|
84
|
-
if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;
|
|
85
|
-
return "https://api.individual.githubcopilot.com";
|
|
100
|
+
export function getGitHubCopilotBaseUrl(enterpriseDomain?: string): string {
|
|
101
|
+
const normalizedEnterpriseDomain = normalizeGitHubCopilotEnterpriseDomain(enterpriseDomain);
|
|
102
|
+
if (!normalizedEnterpriseDomain) return "https://api.githubcopilot.com";
|
|
103
|
+
const host = normalizedEnterpriseDomain.startsWith("copilot-api.")
|
|
104
|
+
? normalizedEnterpriseDomain
|
|
105
|
+
: `copilot-api.${normalizedEnterpriseDomain}`;
|
|
106
|
+
return `https://${host}`;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
async function fetchJson(url: string, init: RequestInit): Promise<unknown> {
|
|
@@ -100,10 +121,10 @@ async function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {
|
|
|
100
121
|
method: "POST",
|
|
101
122
|
headers: {
|
|
102
123
|
Accept: "application/json",
|
|
103
|
-
"Content-Type": "application/
|
|
104
|
-
|
|
124
|
+
"Content-Type": "application/json",
|
|
125
|
+
...OPENCODE_HEADERS,
|
|
105
126
|
},
|
|
106
|
-
body:
|
|
127
|
+
body: JSON.stringify({
|
|
107
128
|
client_id: CLIENT_ID,
|
|
108
129
|
scope: "read:user",
|
|
109
130
|
}),
|
|
@@ -172,10 +193,10 @@ async function pollForGitHubAccessToken(
|
|
|
172
193
|
method: "POST",
|
|
173
194
|
headers: {
|
|
174
195
|
Accept: "application/json",
|
|
175
|
-
"Content-Type": "application/
|
|
176
|
-
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
...OPENCODE_HEADERS,
|
|
177
198
|
},
|
|
178
|
-
body:
|
|
199
|
+
body: JSON.stringify({
|
|
179
200
|
client_id: CLIENT_ID,
|
|
180
201
|
device_code: deviceCode,
|
|
181
202
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
@@ -214,39 +235,18 @@ async function pollForGitHubAccessToken(
|
|
|
214
235
|
throw new Error("Device flow timed out");
|
|
215
236
|
}
|
|
216
237
|
|
|
238
|
+
/** Far-future expiry (10 years). GitHub OAuth tokens are long-lived; no JWT exchange needed. */
|
|
239
|
+
const FAR_FUTURE_MS = Date.now() + 10 * 365.25 * 24 * 60 * 60 * 1000;
|
|
240
|
+
|
|
217
241
|
/**
|
|
218
|
-
* Refresh GitHub Copilot token
|
|
242
|
+
* Refresh GitHub Copilot token.
|
|
243
|
+
* With the opencode OAuth flow, the GitHub token is used directly — no JWT exchange needed.
|
|
219
244
|
*/
|
|
220
|
-
export
|
|
221
|
-
refreshToken: string,
|
|
222
|
-
enterpriseDomain?: string,
|
|
223
|
-
): Promise<OAuthCredentials> {
|
|
224
|
-
const domain = enterpriseDomain || "github.com";
|
|
225
|
-
const urls = getUrls(domain);
|
|
226
|
-
|
|
227
|
-
const raw = await fetchJson(urls.copilotTokenUrl, {
|
|
228
|
-
headers: {
|
|
229
|
-
Accept: "application/json",
|
|
230
|
-
Authorization: `Bearer ${refreshToken}`,
|
|
231
|
-
...COPILOT_HEADERS,
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
if (!raw || typeof raw !== "object") {
|
|
236
|
-
throw new Error("Invalid Copilot token response");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const token = (raw as Record<string, unknown>).token;
|
|
240
|
-
const expiresAt = (raw as Record<string, unknown>).expires_at;
|
|
241
|
-
|
|
242
|
-
if (typeof token !== "string" || typeof expiresAt !== "number") {
|
|
243
|
-
throw new Error("Invalid Copilot token response fields");
|
|
244
|
-
}
|
|
245
|
-
|
|
245
|
+
export function refreshGitHubCopilotToken(refreshToken: string, enterpriseDomain?: string): OAuthCredentials {
|
|
246
246
|
return {
|
|
247
247
|
refresh: refreshToken,
|
|
248
|
-
access:
|
|
249
|
-
expires:
|
|
248
|
+
access: refreshToken,
|
|
249
|
+
expires: FAR_FUTURE_MS,
|
|
250
250
|
enterpriseUrl: enterpriseDomain,
|
|
251
251
|
};
|
|
252
252
|
}
|
|
@@ -256,7 +256,7 @@ export async function refreshGitHubCopilotToken(
|
|
|
256
256
|
* This is required for some models (like Claude, Grok) before they can be used.
|
|
257
257
|
*/
|
|
258
258
|
async function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {
|
|
259
|
-
const baseUrl = getGitHubCopilotBaseUrl(
|
|
259
|
+
const baseUrl = getGitHubCopilotBaseUrl(enterpriseDomain);
|
|
260
260
|
const url = `${baseUrl}/models/${modelId}/policy`;
|
|
261
261
|
|
|
262
262
|
try {
|
|
@@ -265,7 +265,7 @@ async function enableGitHubCopilotModel(token: string, modelId: string, enterpri
|
|
|
265
265
|
headers: {
|
|
266
266
|
"Content-Type": "application/json",
|
|
267
267
|
Authorization: `Bearer ${token}`,
|
|
268
|
-
...
|
|
268
|
+
...OPENCODE_HEADERS,
|
|
269
269
|
"openai-intent": "chat-policy",
|
|
270
270
|
"x-interaction-type": "chat-policy",
|
|
271
271
|
},
|
|
@@ -287,12 +287,16 @@ async function enableAllGitHubCopilotModels(
|
|
|
287
287
|
onProgress?: (model: string, success: boolean) => void,
|
|
288
288
|
): Promise<void> {
|
|
289
289
|
const models = getBundledModels("github-copilot");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
const BATCH_SIZE = 5;
|
|
291
|
+
for (let i = 0; i < models.length; i += BATCH_SIZE) {
|
|
292
|
+
const batch = models.slice(i, i + BATCH_SIZE);
|
|
293
|
+
await Promise.all(
|
|
294
|
+
batch.map(async model => {
|
|
295
|
+
const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
|
|
296
|
+
onProgress?.(model.id, success);
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
296
300
|
}
|
|
297
301
|
|
|
298
302
|
/**
|
|
@@ -320,11 +324,13 @@ export async function loginGitHubCopilot(options: {
|
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
const trimmed = input.trim();
|
|
323
|
-
const
|
|
324
|
-
if (trimmed && !
|
|
327
|
+
const normalizedDomain = normalizeDomain(input);
|
|
328
|
+
if (trimmed && !normalizedDomain) {
|
|
325
329
|
throw new Error("Invalid GitHub Enterprise URL/domain");
|
|
326
330
|
}
|
|
327
|
-
const
|
|
331
|
+
const enterpriseDomain = normalizeGitHubCopilotEnterpriseDomain(normalizedDomain ?? undefined);
|
|
332
|
+
const domain =
|
|
333
|
+
normalizedDomain && isPublicGitHubHost(normalizedDomain) ? "github.com" : (normalizedDomain ?? "github.com");
|
|
328
334
|
|
|
329
335
|
const device = await startDeviceFlow(domain);
|
|
330
336
|
options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`);
|
|
@@ -336,10 +342,17 @@ export async function loginGitHubCopilot(options: {
|
|
|
336
342
|
device.expires_in,
|
|
337
343
|
options.signal,
|
|
338
344
|
);
|
|
339
|
-
|
|
345
|
+
|
|
346
|
+
// With opencode OAuth, the GitHub token is used directly for all API requests
|
|
347
|
+
const credentials: OAuthCredentials = {
|
|
348
|
+
refresh: githubAccessToken,
|
|
349
|
+
access: githubAccessToken,
|
|
350
|
+
expires: FAR_FUTURE_MS,
|
|
351
|
+
enterpriseUrl: enterpriseDomain ?? undefined,
|
|
352
|
+
};
|
|
340
353
|
|
|
341
354
|
// Enable all models after successful login
|
|
342
355
|
options.onProgress?.("Enabling models...");
|
|
343
|
-
await enableAllGitHubCopilotModels(
|
|
356
|
+
await enableAllGitHubCopilotModels(githubAccessToken, enterpriseDomain ?? undefined);
|
|
344
357
|
return credentials;
|
|
345
358
|
}
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -437,7 +437,7 @@ function getPerplexityJwtExpiryMs(token: string): number | undefined {
|
|
|
437
437
|
* Get API key for a provider from OAuth credentials.
|
|
438
438
|
* Automatically refreshes expired tokens.
|
|
439
439
|
*
|
|
440
|
-
* For
|
|
440
|
+
* For providers that need credential metadata at request time, returns JSON-encoded credentials
|
|
441
441
|
* plus refresh/expiry metadata for proactive refresh support.
|
|
442
442
|
* @returns API key string, or null if no credentials
|
|
443
443
|
* @throws Error if refresh fails
|
|
@@ -476,11 +476,13 @@ export async function getOAuthApiKey(
|
|
|
476
476
|
throw new Error(`Failed to refresh OAuth token for ${provider}: ${reason}`);
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
|
-
// For providers that need
|
|
480
|
-
const
|
|
481
|
-
|
|
479
|
+
// For providers that need request-time credential metadata, return JSON.
|
|
480
|
+
const needsStructuredApiKey =
|
|
481
|
+
provider === "github-copilot" || provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
482
|
+
const apiKey = needsStructuredApiKey
|
|
482
483
|
? JSON.stringify({
|
|
483
484
|
token: creds.access,
|
|
485
|
+
enterpriseUrl: creds.enterpriseUrl,
|
|
484
486
|
projectId: creds.projectId,
|
|
485
487
|
refreshToken: creds.refresh,
|
|
486
488
|
expiresAt: creds.expires,
|
package/src/utils.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function normalizeResponsesToolCallId(id: string): { callId: string; item
|
|
|
38
38
|
const normalizedItemId = normalizeResponsesItemId(itemId);
|
|
39
39
|
return { callId: normalizedCallId, itemId: normalizedItemId };
|
|
40
40
|
}
|
|
41
|
-
const hash = Bun.hash
|
|
41
|
+
const hash = Bun.hash(id).toString(36);
|
|
42
42
|
const normalizedCallId = id.startsWith("call_") ? truncateResponseItemId(id, "call") : `call_${hash}`;
|
|
43
43
|
return { callId: normalizedCallId, itemId: `fc_${hash}` };
|
|
44
44
|
}
|
|
@@ -51,7 +51,7 @@ function getIdPrefix(id: string, fallback: string): string {
|
|
|
51
51
|
function normalizeResponsesItemId(itemId: string): string {
|
|
52
52
|
const prefix = getIdPrefix(itemId, "fc");
|
|
53
53
|
if (prefix !== "fc" && prefix !== "fcr") {
|
|
54
|
-
return `fc_${Bun.hash
|
|
54
|
+
return `fc_${Bun.hash(itemId).toString(36)}`;
|
|
55
55
|
}
|
|
56
56
|
return truncateResponseItemId(itemId, prefix);
|
|
57
57
|
}
|
|
@@ -62,7 +62,7 @@ function normalizeResponsesItemId(itemId: string): string {
|
|
|
62
62
|
*/
|
|
63
63
|
export function truncateResponseItemId(id: string, prefix: string): string {
|
|
64
64
|
if (id.length <= 64) return id;
|
|
65
|
-
return `${prefix}_${Bun.hash
|
|
65
|
+
return `${prefix}_${Bun.hash(id).toString(36)}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput {
|