@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 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",
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.4",
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",
@@ -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. The actual refresh happens lazily when the
1886
- * provider is used for an API call.
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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.individual.githubcopilot.com",
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": "GitHubCopilotChat/0.35.0",
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": "GitHubCopilotChat/0.35.0",
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 apiKey = config?.apiKey;
1481
- const configuredBaseUrl = config?.baseUrl ?? "https://api.individual.githubcopilot.com";
1482
- const baseUrl =
1483
- apiKey?.includes("proxy-ep=") && configuredBaseUrl.includes("githubcopilot.com")
1484
- ? getGitHubCopilotBaseUrl(apiKey)
1485
- : configuredBaseUrl;
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: GITHUB_COPILOT_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: { ...GITHUB_COPILOT_HEADERS, ...(providerReference?.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: { ...GITHUB_COPILOT_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.individual.githubcopilot.com";
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: { ...COPILOT_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 ${apiKey}`,
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: apiKey,
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?.includes("proxy-ep=")) return baseUrl;
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(apiKey);
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.xxHash64(msgId).toString(36)}`;
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, apiKey) ?? 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.xxHash64(msgId).toString(36)}`;
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, apiKey) ?? 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
- ...COPILOT_HEADERS,
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 = 60_000;
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 decode = (s: string) => atob(s);
9
- const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=");
8
+ const CLIENT_ID = "Ov23li8tweQw6odWQebz";
10
9
 
11
- const COPILOT_HEADERS = {
12
- "User-Agent": "GitHubCopilotChat/0.35.0",
13
- "Editor-Version": "vscode/1.107.0",
14
- "Editor-Plugin-Version": "copilot-chat/0.35.0",
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
- * Parse the proxy-ep from a Copilot token and convert to API base URL.
65
- * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...
66
- * Returns API URL like https://api.individual.githubcopilot.com
67
- */
68
- function getBaseUrlFromToken(token: string): string | null {
69
- const match = token.match(/proxy-ep=([^;]+)/);
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/x-www-form-urlencoded",
104
- "User-Agent": "GitHubCopilotChat/0.35.0",
124
+ "Content-Type": "application/json",
125
+ ...OPENCODE_HEADERS,
105
126
  },
106
- body: new URLSearchParams({
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/x-www-form-urlencoded",
176
- "User-Agent": "GitHubCopilotChat/0.35.0",
196
+ "Content-Type": "application/json",
197
+ ...OPENCODE_HEADERS,
177
198
  },
178
- body: new URLSearchParams({
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 async function refreshGitHubCopilotToken(
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: token,
249
- expires: expiresAt * 1000 - 5 * 60 * 1000,
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(token, enterpriseDomain);
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
- ...COPILOT_HEADERS,
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
- await Promise.all(
291
- models.map(async model => {
292
- const success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);
293
- onProgress?.(model.id, success);
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 enterpriseDomain = normalizeDomain(input);
324
- if (trimmed && !enterpriseDomain) {
327
+ const normalizedDomain = normalizeDomain(input);
328
+ if (trimmed && !normalizedDomain) {
325
329
  throw new Error("Invalid GitHub Enterprise URL/domain");
326
330
  }
327
- const domain = enterpriseDomain || "github.com";
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
- const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined);
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(credentials.access, enterpriseDomain ?? undefined);
356
+ await enableAllGitHubCopilotModels(githubAccessToken, enterpriseDomain ?? undefined);
344
357
  return credentials;
345
358
  }
@@ -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 google-gemini-cli and antigravity, returns JSON-encoded credentials including token/projectId
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 projectId, return JSON
480
- const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
481
- const apiKey = needsProjectId
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.xxHash64(id).toString(36);
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.xxHash64(itemId).toString(36)}`;
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.xxHash64(id).toString(36)}`;
65
+ return `${prefix}_${Bun.hash(id).toString(36)}`;
66
66
  }
67
67
 
68
68
  export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput {