@ottocode/sdk 0.1.225 → 0.1.227

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -101,7 +101,7 @@
101
101
  "@modelcontextprotocol/sdk": "^1.12",
102
102
  "@openauthjs/openauth": "^0.4.3",
103
103
  "@openrouter/ai-sdk-provider": "^1.2.0",
104
- "@ottocode/ai-sdk": "0.1.8",
104
+ "@ottocode/ai-sdk": "0.2.0",
105
105
  "@solana/web3.js": "^1.98.0",
106
106
  "ai": "^6.0.0",
107
107
  "bs58": "^6.0.0",
@@ -9,7 +9,24 @@ import type { OttoConfig } from '../../types/src/index.ts';
9
9
 
10
10
  export type { OttoConfig } from '../../types/src/index.ts';
11
11
 
12
- const DEFAULTS: { defaults: OttoConfig['defaults'] } = {
12
+ const DEFAULT_PROVIDER_SETTINGS: OttoConfig['providers'] = {
13
+ openai: { enabled: false },
14
+ anthropic: { enabled: false },
15
+ google: { enabled: false },
16
+ openrouter: { enabled: false },
17
+ opencode: { enabled: false },
18
+ copilot: { enabled: false },
19
+ setu: { enabled: true },
20
+ zai: { enabled: false },
21
+ 'zai-coding': { enabled: false },
22
+ moonshot: { enabled: false },
23
+ minimax: { enabled: false },
24
+ };
25
+
26
+ const DEFAULTS: {
27
+ defaults: OttoConfig['defaults'];
28
+ providers: OttoConfig['providers'];
29
+ } = {
13
30
  defaults: {
14
31
  agent: 'build',
15
32
  provider: 'setu',
@@ -18,6 +35,7 @@ const DEFAULTS: { defaults: OttoConfig['defaults'] } = {
18
35
  guidedMode: false,
19
36
  reasoningText: true,
20
37
  },
38
+ providers: DEFAULT_PROVIDER_SETTINGS,
21
39
  };
22
40
 
23
41
  export async function loadConfig(
@@ -42,6 +60,7 @@ export async function loadConfig(
42
60
  return {
43
61
  projectRoot,
44
62
  defaults: merged.defaults as OttoConfig['defaults'],
63
+ providers: merged.providers as OttoConfig['providers'],
45
64
  paths: {
46
65
  dataDir,
47
66
  dbPath,
@@ -65,6 +65,7 @@ export async function writeDefaults(
65
65
  model: string;
66
66
  toolApproval: 'auto' | 'dangerous' | 'all';
67
67
  guidedMode: boolean;
68
+ reasoningText: boolean;
68
69
  theme: string;
69
70
  }>,
70
71
  projectRoot?: string,
@@ -1,15 +1,20 @@
1
+ // @ts-expect-error Bun file asset import
1
2
  import darwinArm64 from 'bun-pty/rust-pty/target/release/librust_pty_arm64.dylib' with {
2
3
  type: 'file',
3
4
  };
5
+ // @ts-expect-error Bun file asset import
4
6
  import darwinX64 from 'bun-pty/rust-pty/target/release/librust_pty.dylib' with {
5
7
  type: 'file',
6
8
  };
9
+ // @ts-expect-error Bun file asset import
7
10
  import linuxArm64 from 'bun-pty/rust-pty/target/release/librust_pty_arm64.so' with {
8
11
  type: 'file',
9
12
  };
13
+ // @ts-expect-error Bun file asset import
10
14
  import linuxX64 from 'bun-pty/rust-pty/target/release/librust_pty.so' with {
11
15
  type: 'file',
12
16
  };
17
+ // @ts-expect-error Bun file asset import
13
18
  import windowsDll from 'bun-pty/rust-pty/target/release/rust_pty.dll' with {
14
19
  type: 'file',
15
20
  };
@@ -16,6 +16,11 @@ type ParsedBody = {
16
16
  [key: string]: unknown;
17
17
  };
18
18
 
19
+ type FetchLike = (
20
+ input: Parameters<typeof fetch>[0],
21
+ init?: Parameters<typeof fetch>[1],
22
+ ) => Promise<Response>;
23
+
19
24
  export function addAnthropicCacheControl(parsed: ParsedBody): ParsedBody {
20
25
  const MAX_SYSTEM_CACHE = 1;
21
26
  const MAX_MESSAGE_CACHE = 1;
@@ -84,8 +89,8 @@ export function addAnthropicCacheControl(parsed: ParsedBody): ParsedBody {
84
89
  return parsed;
85
90
  }
86
91
 
87
- export function createAnthropicCachingFetch(): typeof fetch {
88
- return async (input: RequestInfo | URL, init?: RequestInit) => {
92
+ export function createAnthropicCachingFetch(): FetchLike {
93
+ return async (input, init) => {
89
94
  let body = init?.body;
90
95
  if (body && typeof body === 'string') {
91
96
  try {
@@ -103,8 +108,8 @@ export function createAnthropicCachingFetch(): typeof fetch {
103
108
  export function createConditionalCachingFetch(
104
109
  shouldCache: (model: string) => boolean,
105
110
  model: string,
106
- ): typeof fetch {
107
- return async (input: RequestInfo | URL, init?: RequestInit) => {
111
+ ): FetchLike {
112
+ return async (input, init) => {
108
113
  if (!shouldCache(model)) {
109
114
  return fetch(input, init);
110
115
  }
@@ -3,6 +3,11 @@ import { addAnthropicCacheControl } from './anthropic-caching.ts';
3
3
 
4
4
  const CLAUDE_CLI_VERSION = '1.0.61';
5
5
 
6
+ type FetchLike = (
7
+ input: Parameters<typeof fetch>[0],
8
+ init?: Parameters<typeof fetch>[1],
9
+ ) => Promise<Response>;
10
+
6
11
  export type AnthropicOAuthConfig = {
7
12
  oauth: {
8
13
  access: string;
@@ -42,7 +47,7 @@ function buildOAuthHeaders(accessToken: string): Record<string, string> {
42
47
  }
43
48
 
44
49
  function filterExistingHeaders(
45
- initHeaders: HeadersInit | undefined,
50
+ initHeaders: RequestInit['headers'] | undefined,
46
51
  ): Record<string, string> {
47
52
  const headers: Record<string, string> = {};
48
53
  if (!initHeaders) return headers;
@@ -75,7 +80,7 @@ function filterExistingHeaders(
75
80
 
76
81
  export function createAnthropicOAuthFetch(
77
82
  config: AnthropicOAuthConfig,
78
- ): typeof fetch {
83
+ ): FetchLike {
79
84
  const { oauth, toolNameTransformer } = config;
80
85
 
81
86
  return async (input: string | URL | Request, init?: RequestInit) => {
@@ -757,6 +757,57 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
757
757
  output: 32000,
758
758
  },
759
759
  },
760
+ {
761
+ id: 'gpt-5.4',
762
+ ownedBy: 'openai',
763
+ label: 'GPT-5.4',
764
+ modalities: {
765
+ input: ['text', 'image'],
766
+ output: ['text'],
767
+ },
768
+ toolCall: true,
769
+ reasoningText: true,
770
+ attachment: true,
771
+ temperature: false,
772
+ knowledge: '2025-08-31',
773
+ releaseDate: '2026-03-05',
774
+ lastUpdated: '2026-03-05',
775
+ openWeights: false,
776
+ cost: {
777
+ input: 2.5,
778
+ output: 15,
779
+ cacheRead: 0.25,
780
+ },
781
+ limit: {
782
+ context: 1050000,
783
+ output: 128000,
784
+ },
785
+ },
786
+ {
787
+ id: 'gpt-5.4-pro',
788
+ ownedBy: 'openai',
789
+ label: 'GPT-5.4 Pro',
790
+ modalities: {
791
+ input: ['text', 'image'],
792
+ output: ['text'],
793
+ },
794
+ toolCall: true,
795
+ reasoningText: true,
796
+ attachment: true,
797
+ temperature: false,
798
+ knowledge: '2025-08-31',
799
+ releaseDate: '2026-03-05',
800
+ lastUpdated: '2026-03-05',
801
+ openWeights: false,
802
+ cost: {
803
+ input: 30,
804
+ output: 180,
805
+ },
806
+ limit: {
807
+ context: 1050000,
808
+ output: 128000,
809
+ },
810
+ },
760
811
  {
761
812
  id: 'o1',
762
813
  ownedBy: 'openai',
@@ -3614,6 +3665,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
3614
3665
  output: 65536,
3615
3666
  },
3616
3667
  },
3668
+ {
3669
+ id: 'google/gemini-3.1-pro-preview-customtools',
3670
+ ownedBy: 'google',
3671
+ label: 'Gemini 3.1 Pro Preview Custom Tools',
3672
+ modalities: {
3673
+ input: ['text', 'image', 'audio', 'video', 'pdf'],
3674
+ output: ['text'],
3675
+ },
3676
+ toolCall: true,
3677
+ reasoningText: true,
3678
+ attachment: true,
3679
+ temperature: true,
3680
+ knowledge: '2025-01',
3681
+ releaseDate: '2026-02-19',
3682
+ lastUpdated: '2026-02-19',
3683
+ openWeights: false,
3684
+ cost: {
3685
+ input: 2,
3686
+ output: 12,
3687
+ },
3688
+ limit: {
3689
+ context: 1048576,
3690
+ output: 65536,
3691
+ },
3692
+ },
3617
3693
  {
3618
3694
  id: 'google/gemma-2-9b-it',
3619
3695
  ownedBy: 'google',
@@ -5396,6 +5472,84 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
5396
5472
  output: 128000,
5397
5473
  },
5398
5474
  },
5475
+ {
5476
+ id: 'openai/gpt-5.3-codex',
5477
+ ownedBy: 'openai',
5478
+ label: 'GPT-5.3-Codex',
5479
+ modalities: {
5480
+ input: ['text', 'image', 'pdf'],
5481
+ output: ['text'],
5482
+ },
5483
+ toolCall: true,
5484
+ reasoningText: true,
5485
+ attachment: true,
5486
+ temperature: false,
5487
+ knowledge: '2025-08-31',
5488
+ releaseDate: '2026-02-24',
5489
+ lastUpdated: '2026-02-24',
5490
+ openWeights: false,
5491
+ cost: {
5492
+ input: 1.75,
5493
+ output: 14,
5494
+ cacheRead: 0.175,
5495
+ },
5496
+ limit: {
5497
+ context: 400000,
5498
+ output: 128000,
5499
+ },
5500
+ },
5501
+ {
5502
+ id: 'openai/gpt-5.4',
5503
+ ownedBy: 'openai',
5504
+ label: 'GPT-5.4',
5505
+ modalities: {
5506
+ input: ['text', 'image', 'pdf'],
5507
+ output: ['text'],
5508
+ },
5509
+ toolCall: true,
5510
+ reasoningText: true,
5511
+ attachment: true,
5512
+ temperature: false,
5513
+ knowledge: '2025-08-31',
5514
+ releaseDate: '2026-03-05',
5515
+ lastUpdated: '2026-03-05',
5516
+ openWeights: false,
5517
+ cost: {
5518
+ input: 2.5,
5519
+ output: 15,
5520
+ cacheRead: 0.25,
5521
+ },
5522
+ limit: {
5523
+ context: 1050000,
5524
+ output: 128000,
5525
+ },
5526
+ },
5527
+ {
5528
+ id: 'openai/gpt-5.4-pro',
5529
+ ownedBy: 'openai',
5530
+ label: 'GPT-5.4 Pro',
5531
+ modalities: {
5532
+ input: ['text', 'image', 'pdf'],
5533
+ output: ['text'],
5534
+ },
5535
+ toolCall: true,
5536
+ reasoningText: true,
5537
+ attachment: true,
5538
+ temperature: false,
5539
+ knowledge: '2025-08-31',
5540
+ releaseDate: '2026-03-05',
5541
+ lastUpdated: '2026-03-05',
5542
+ openWeights: false,
5543
+ cost: {
5544
+ input: 30,
5545
+ output: 180,
5546
+ cacheRead: 30,
5547
+ },
5548
+ limit: {
5549
+ context: 1050000,
5550
+ output: 128000,
5551
+ },
5552
+ },
5399
5553
  {
5400
5554
  id: 'openai/gpt-oss-120b',
5401
5555
  ownedBy: 'openai',
@@ -7799,6 +7953,122 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
7799
7953
  npm: '@ai-sdk/openai',
7800
7954
  },
7801
7955
  },
7956
+ {
7957
+ id: 'gpt-5.3-codex',
7958
+ ownedBy: 'openai',
7959
+ label: 'GPT-5.3 Codex',
7960
+ modalities: {
7961
+ input: ['text', 'image', 'pdf'],
7962
+ output: ['text'],
7963
+ },
7964
+ toolCall: true,
7965
+ reasoningText: true,
7966
+ attachment: true,
7967
+ temperature: false,
7968
+ knowledge: '2025-08-31',
7969
+ releaseDate: '2026-02-24',
7970
+ lastUpdated: '2026-02-24',
7971
+ openWeights: false,
7972
+ cost: {
7973
+ input: 1.75,
7974
+ output: 14,
7975
+ cacheRead: 0.175,
7976
+ },
7977
+ limit: {
7978
+ context: 400000,
7979
+ output: 128000,
7980
+ },
7981
+ provider: {
7982
+ npm: '@ai-sdk/openai',
7983
+ },
7984
+ },
7985
+ {
7986
+ id: 'gpt-5.3-codex-spark',
7987
+ ownedBy: 'openai',
7988
+ label: 'GPT-5.3 Codex Spark',
7989
+ modalities: {
7990
+ input: ['text'],
7991
+ output: ['text'],
7992
+ },
7993
+ toolCall: true,
7994
+ reasoningText: true,
7995
+ attachment: false,
7996
+ temperature: false,
7997
+ knowledge: '2025-08-31',
7998
+ releaseDate: '2026-02-12',
7999
+ lastUpdated: '2026-02-12',
8000
+ openWeights: false,
8001
+ cost: {
8002
+ input: 1.75,
8003
+ output: 14,
8004
+ cacheRead: 0.175,
8005
+ },
8006
+ limit: {
8007
+ context: 128000,
8008
+ output: 128000,
8009
+ },
8010
+ provider: {
8011
+ npm: '@ai-sdk/openai',
8012
+ },
8013
+ },
8014
+ {
8015
+ id: 'gpt-5.4',
8016
+ ownedBy: 'openai',
8017
+ label: 'GPT-5.4',
8018
+ modalities: {
8019
+ input: ['text', 'image', 'pdf'],
8020
+ output: ['text'],
8021
+ },
8022
+ toolCall: true,
8023
+ reasoningText: true,
8024
+ attachment: true,
8025
+ temperature: false,
8026
+ knowledge: '2025-08-31',
8027
+ releaseDate: '2026-03-05',
8028
+ lastUpdated: '2026-03-05',
8029
+ openWeights: false,
8030
+ cost: {
8031
+ input: 2.5,
8032
+ output: 15,
8033
+ cacheRead: 0.25,
8034
+ },
8035
+ limit: {
8036
+ context: 1050000,
8037
+ output: 128000,
8038
+ },
8039
+ provider: {
8040
+ npm: '@ai-sdk/openai',
8041
+ },
8042
+ },
8043
+ {
8044
+ id: 'gpt-5.4-pro',
8045
+ ownedBy: 'openai',
8046
+ label: 'GPT-5.4 Pro',
8047
+ modalities: {
8048
+ input: ['text', 'image', 'pdf'],
8049
+ output: ['text'],
8050
+ },
8051
+ toolCall: true,
8052
+ reasoningText: true,
8053
+ attachment: true,
8054
+ temperature: false,
8055
+ knowledge: '2025-08-31',
8056
+ releaseDate: '2026-03-05',
8057
+ lastUpdated: '2026-03-05',
8058
+ openWeights: false,
8059
+ cost: {
8060
+ input: 30,
8061
+ output: 180,
8062
+ cacheRead: 30,
8063
+ },
8064
+ limit: {
8065
+ context: 1050000,
8066
+ output: 128000,
8067
+ },
8068
+ provider: {
8069
+ npm: '@ai-sdk/openai',
8070
+ },
8071
+ },
7802
8072
  {
7803
8073
  id: 'grok-code',
7804
8074
  ownedBy: 'xai',
@@ -7900,7 +8170,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
7900
8170
  },
7901
8171
  limit: {
7902
8172
  context: 262144,
7903
- output: 262144,
8173
+ output: 65536,
7904
8174
  },
7905
8175
  },
7906
8176
  {
@@ -9145,6 +9415,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
9145
9415
  output: 64000,
9146
9416
  },
9147
9417
  },
9418
+ {
9419
+ id: 'gemini-3.1-pro-preview',
9420
+ ownedBy: 'google',
9421
+ label: 'Gemini 3.1 Pro Preview',
9422
+ modalities: {
9423
+ input: ['text', 'image'],
9424
+ output: ['text'],
9425
+ },
9426
+ toolCall: true,
9427
+ reasoningText: true,
9428
+ attachment: true,
9429
+ temperature: true,
9430
+ knowledge: '2025-01',
9431
+ releaseDate: '2026-02-19',
9432
+ lastUpdated: '2026-02-19',
9433
+ openWeights: false,
9434
+ cost: {
9435
+ input: 0,
9436
+ output: 0,
9437
+ },
9438
+ limit: {
9439
+ context: 128000,
9440
+ output: 64000,
9441
+ },
9442
+ },
9148
9443
  {
9149
9444
  id: 'gpt-4.1',
9150
9445
  ownedBy: 'openai',
@@ -9395,6 +9690,56 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
9395
9690
  output: 128000,
9396
9691
  },
9397
9692
  },
9693
+ {
9694
+ id: 'gpt-5.3-codex',
9695
+ ownedBy: 'openai',
9696
+ label: 'GPT-5.3-Codex',
9697
+ modalities: {
9698
+ input: ['text', 'image'],
9699
+ output: ['text'],
9700
+ },
9701
+ toolCall: true,
9702
+ reasoningText: true,
9703
+ attachment: false,
9704
+ temperature: false,
9705
+ knowledge: '2025-08-31',
9706
+ releaseDate: '2026-02-24',
9707
+ lastUpdated: '2026-02-24',
9708
+ openWeights: false,
9709
+ cost: {
9710
+ input: 0,
9711
+ output: 0,
9712
+ },
9713
+ limit: {
9714
+ context: 400000,
9715
+ output: 128000,
9716
+ },
9717
+ },
9718
+ {
9719
+ id: 'gpt-5.4',
9720
+ ownedBy: 'openai',
9721
+ label: 'GPT-5.4',
9722
+ modalities: {
9723
+ input: ['text', 'image'],
9724
+ output: ['text'],
9725
+ },
9726
+ toolCall: true,
9727
+ reasoningText: true,
9728
+ attachment: false,
9729
+ temperature: false,
9730
+ knowledge: '2025-08-31',
9731
+ releaseDate: '2026-03-05',
9732
+ lastUpdated: '2026-03-05',
9733
+ openWeights: false,
9734
+ cost: {
9735
+ input: 0,
9736
+ output: 0,
9737
+ },
9738
+ limit: {
9739
+ context: 400000,
9740
+ output: 128000,
9741
+ },
9742
+ },
9398
9743
  {
9399
9744
  id: 'grok-code-fast-1',
9400
9745
  ownedBy: 'xai',
@@ -1,4 +1,4 @@
1
- import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
2
  import type { OAuth } from '../../types/src/index.ts';
3
3
 
4
4
  const COPILOT_BASE_URL = 'https://api.githubcopilot.com';
@@ -7,7 +7,155 @@ export type CopilotOAuthConfig = {
7
7
  oauth: OAuth;
8
8
  };
9
9
 
10
- export function createCopilotFetch(config: CopilotOAuthConfig): typeof fetch {
10
+ const COPILOT_REASONING_DROP_TYPES = new Set([
11
+ 'response.reasoning.delta',
12
+ 'response.reasoning.done',
13
+ 'response.reasoning_summary_part.added',
14
+ 'response.reasoning_summary_text.delta',
15
+ 'response.reasoning_summary_part.done',
16
+ ]);
17
+
18
+ function shouldDropCopilotEvent(data: string): boolean {
19
+ try {
20
+ const parsed = JSON.parse(data) as Record<string, unknown>;
21
+ const type = typeof parsed.type === 'string' ? parsed.type : '';
22
+ if (!type) return false;
23
+
24
+ if (COPILOT_REASONING_DROP_TYPES.has(type)) return true;
25
+
26
+ if (
27
+ (type === 'response.output_item.added' ||
28
+ type === 'response.output_item.done') &&
29
+ parsed.item &&
30
+ typeof parsed.item === 'object'
31
+ ) {
32
+ return (parsed.item as Record<string, unknown>).type === 'reasoning';
33
+ }
34
+
35
+ return false;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function filterSseEvent(rawEvent: string): string | null {
42
+ if (!rawEvent.trim()) return rawEvent;
43
+
44
+ const dataLines: string[] = [];
45
+ for (const line of rawEvent.split('\n')) {
46
+ if (line.startsWith('data:')) {
47
+ dataLines.push(line.slice('data:'.length).trimStart());
48
+ }
49
+ }
50
+
51
+ if (!dataLines.length) return rawEvent;
52
+
53
+ const data = dataLines.join('\n');
54
+ if (data === '[DONE]') return rawEvent;
55
+
56
+ if (shouldDropCopilotEvent(data)) return null;
57
+ return rawEvent;
58
+ }
59
+
60
+ const SYNTHETIC_COMPLETED =
61
+ 'data: {"type":"response.completed","response":{"status":"completed","incomplete_details":null,"usage":{"input_tokens":0,"output_tokens":0}}}';
62
+
63
+ function sanitizeCopilotResponsesStream(response: Response): Response {
64
+ if (!response.body) return response;
65
+
66
+ const decoder = new TextDecoder();
67
+ const encoder = new TextEncoder();
68
+ let buffer = '';
69
+ let seenCompleted = false;
70
+ let seenDone = false;
71
+
72
+ function processBuffer(
73
+ controller: TransformStreamDefaultController<Uint8Array>,
74
+ ) {
75
+ let boundary = buffer.indexOf('\n\n');
76
+ while (boundary !== -1) {
77
+ const rawEvent = buffer.slice(0, boundary);
78
+ buffer = buffer.slice(boundary + 2);
79
+
80
+ const filtered = filterSseEvent(rawEvent);
81
+ if (filtered !== null) {
82
+ const dataLine = filtered
83
+ .split('\n')
84
+ .find((l) => l.startsWith('data:'));
85
+ const d = dataLine?.slice(5).trim();
86
+ if (d === '[DONE]') {
87
+ seenDone = true;
88
+ } else if (d) {
89
+ try {
90
+ const p = JSON.parse(d) as Record<string, unknown>;
91
+ if (
92
+ p.type === 'response.completed' ||
93
+ p.type === 'response.incomplete'
94
+ ) {
95
+ seenCompleted = true;
96
+ }
97
+ } catch {}
98
+ }
99
+ controller.enqueue(encoder.encode(`${filtered}\n\n`));
100
+ }
101
+
102
+ boundary = buffer.indexOf('\n\n');
103
+ }
104
+ }
105
+
106
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
107
+ transform(chunk, controller) {
108
+ buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
109
+ processBuffer(controller);
110
+ },
111
+ flush(controller) {
112
+ buffer += decoder.decode().replace(/\r\n/g, '\n');
113
+ if (buffer.trim()) {
114
+ buffer += '\n\n';
115
+ processBuffer(controller);
116
+ }
117
+ if (!seenCompleted) {
118
+ controller.enqueue(encoder.encode(`${SYNTHETIC_COMPLETED}\n\n`));
119
+ }
120
+ if (!seenDone) {
121
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
122
+ }
123
+ },
124
+ });
125
+
126
+ return new Response(response.body.pipeThrough(transform), {
127
+ status: response.status,
128
+ statusText: response.statusText,
129
+ headers: response.headers,
130
+ });
131
+ }
132
+
133
+ function sanitizeCopilotRequestBody(body: string): string {
134
+ try {
135
+ const parsed = JSON.parse(body);
136
+ delete parsed.store;
137
+ delete parsed.previous_response_id;
138
+ if (Array.isArray(parsed.input)) {
139
+ for (const item of parsed.input) {
140
+ if (item && typeof item === 'object') {
141
+ if (item.type === 'function_call' && 'id' in item) {
142
+ delete item.id;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return JSON.stringify(parsed);
148
+ } catch {
149
+ return body;
150
+ }
151
+ }
152
+
153
+ export function createCopilotFetch(
154
+ config: CopilotOAuthConfig,
155
+ ): (
156
+ input: Parameters<typeof fetch>[0],
157
+ init?: Parameters<typeof fetch>[1],
158
+ ) => Promise<Response> {
11
159
  return async (
12
160
  input: string | URL | Request,
13
161
  init?: RequestInit,
@@ -19,21 +167,50 @@ export function createCopilotFetch(config: CopilotOAuthConfig): typeof fetch {
19
167
  headers.set('Openai-Intent', 'conversation-edits');
20
168
  headers.set('User-Agent', 'ottocode');
21
169
 
22
- return fetch(input, {
170
+ const requestUrl =
171
+ typeof input === 'string'
172
+ ? input
173
+ : input instanceof URL
174
+ ? input.href
175
+ : input.url;
176
+
177
+ if (requestUrl.includes('/responses') && typeof init?.body === 'string') {
178
+ init = { ...init, body: sanitizeCopilotRequestBody(init.body) };
179
+ }
180
+
181
+ const response = await fetch(input, {
23
182
  ...init,
24
183
  headers,
25
184
  });
185
+
186
+ if (requestUrl.includes('/responses') && response.ok) {
187
+ return sanitizeCopilotResponsesStream(response);
188
+ }
189
+
190
+ return response;
26
191
  };
27
192
  }
28
193
 
194
+ function isGpt5OrLater(model: string): boolean {
195
+ const match = /^gpt-(\d+)/.exec(model);
196
+ if (!match) return false;
197
+ return Number(match[1]) >= 5;
198
+ }
199
+
200
+ function needsResponsesApi(model: string): boolean {
201
+ return isGpt5OrLater(model) && !model.startsWith('gpt-5-mini');
202
+ }
203
+
29
204
  export function createCopilotModel(model: string, config: CopilotOAuthConfig) {
30
205
  const customFetch = createCopilotFetch(config);
31
206
 
32
- const provider = createOpenAICompatible({
33
- name: 'github-copilot',
34
- baseURL: COPILOT_BASE_URL,
207
+ const provider = createOpenAI({
35
208
  apiKey: 'copilot-oauth',
36
- fetch: customFetch,
209
+ baseURL: COPILOT_BASE_URL,
210
+ fetch: customFetch as typeof fetch,
37
211
  });
38
- return provider.chatModel(model);
212
+
213
+ return needsResponsesApi(model)
214
+ ? provider.responses(model)
215
+ : provider.chat(model);
39
216
  }
@@ -19,6 +19,14 @@ export function providerEnvVar(provider: ProviderId): string {
19
19
  }
20
20
 
21
21
  export function readEnvKey(provider: ProviderId): string | undefined {
22
+ if (provider === 'copilot') {
23
+ const copilotToken =
24
+ process.env.COPILOT_GITHUB_TOKEN ??
25
+ process.env.GH_TOKEN ??
26
+ process.env.GITHUB_TOKEN;
27
+ return copilotToken?.length ? copilotToken : undefined;
28
+ }
29
+
22
30
  const key = providerEnvVar(provider);
23
31
  const value = process.env[key];
24
32
  return value?.length ? value : undefined;
@@ -8,22 +8,35 @@ const OAUTH_MODEL_PREFIXES: Partial<Record<ProviderId, string[]>> = {
8
8
  'claude-sonnet-4-5',
9
9
  'claude-sonnet-4-6',
10
10
  ],
11
+ };
12
+
13
+ const OAUTH_MODEL_IDS: Partial<Record<ProviderId, string[]>> = {
11
14
  openai: [
12
- 'gpt-5.2-codex',
13
- 'gpt-5.3-codex',
15
+ 'gpt-5.1-codex',
14
16
  'gpt-5.1-codex-max',
15
17
  'gpt-5.1-codex-mini',
16
18
  'gpt-5.2',
19
+ 'gpt-5.2-codex',
20
+ 'gpt-5.3-codex',
21
+ 'gpt-5.4',
17
22
  ],
18
23
  };
19
24
 
25
+ function matchesOAuthModel(provider: ProviderId, modelId: string): boolean {
26
+ const exactIds = OAUTH_MODEL_IDS[provider];
27
+ if (exactIds?.includes(modelId)) return true;
28
+
29
+ const prefixes = OAUTH_MODEL_PREFIXES[provider];
30
+ if (prefixes?.some((prefix) => modelId.startsWith(prefix))) return true;
31
+
32
+ return !exactIds && !prefixes;
33
+ }
34
+
20
35
  export function isModelAllowedForOAuth(
21
36
  provider: ProviderId,
22
37
  modelId: string,
23
38
  ): boolean {
24
- const prefixes = OAUTH_MODEL_PREFIXES[provider];
25
- if (!prefixes) return true;
26
- return prefixes.some((prefix) => modelId.startsWith(prefix));
39
+ return matchesOAuthModel(provider, modelId);
27
40
  }
28
41
 
29
42
  export function filterModelsForAuthType(
@@ -32,15 +45,14 @@ export function filterModelsForAuthType(
32
45
  authType: 'api' | 'oauth' | 'wallet' | undefined,
33
46
  ): ModelInfo[] {
34
47
  if (authType !== 'oauth') return models;
48
+ const exactIds = OAUTH_MODEL_IDS[provider];
35
49
  const prefixes = OAUTH_MODEL_PREFIXES[provider];
36
- if (!prefixes) return models;
37
- return models.filter((m) =>
38
- prefixes.some((prefix) => m.id.startsWith(prefix)),
39
- );
50
+ if (!exactIds && !prefixes) return models;
51
+ return models.filter((model) => matchesOAuthModel(provider, model.id));
40
52
  }
41
53
 
42
54
  export function getOAuthModelPrefixes(
43
55
  provider: ProviderId,
44
56
  ): string[] | undefined {
45
- return OAUTH_MODEL_PREFIXES[provider];
57
+ return OAUTH_MODEL_PREFIXES[provider] ?? OAUTH_MODEL_IDS[provider];
46
58
  }
@@ -135,8 +135,7 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
135
135
  const response = await fetch(targetUrl, {
136
136
  ...init,
137
137
  headers,
138
- // biome-ignore lint/suspicious/noTsIgnore: Bun-specific fetch option
139
- // @ts-ignore
138
+ // @ts-expect-error Bun-specific fetch option
140
139
  timeout: false,
141
140
  });
142
141
 
@@ -165,8 +164,7 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
165
164
  return fetch(targetUrl, {
166
165
  ...init,
167
166
  headers: retryHeaders,
168
- // biome-ignore lint/suspicious/noTsIgnore: Bun-specific fetch option
169
- // @ts-ignore
167
+ // @ts-expect-error Bun-specific fetch option
170
168
  timeout: false,
171
169
  });
172
170
  } catch {
@@ -19,7 +19,10 @@ export function getOpenRouterInstance(
19
19
  const customFetch = model
20
20
  ? createConditionalCachingFetch(isAnthropicModel, model)
21
21
  : undefined;
22
- return createOpenRouter({ apiKey, fetch: customFetch });
22
+ return createOpenRouter({
23
+ apiKey,
24
+ fetch: customFetch as typeof fetch | undefined,
25
+ });
23
26
  }
24
27
 
25
28
  export function createOpenRouterModel(
@@ -88,6 +88,9 @@ const pricingTable: Record<ProviderName, PricingEntry[]> = {
88
88
  moonshot: [
89
89
  // Pricing from catalog entries; leave empty here
90
90
  ],
91
+ minimax: [
92
+ // Pricing from catalog entries; leave empty here
93
+ ],
91
94
  copilot: [],
92
95
  };
93
96
 
@@ -39,6 +39,11 @@ const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
39
39
  copilot: ['gpt-4.1-mini'],
40
40
  };
41
41
 
42
+ const PREFERRED_FAST_MODELS_OAUTH: Partial<Record<ProviderId, string[]>> = {
43
+ openai: ['gpt-5.1-codex-mini'],
44
+ anthropic: ['claude-haiku-4-5'],
45
+ };
46
+
42
47
  export function getFastModel(provider: ProviderId): string | undefined {
43
48
  const providerModels = catalog[provider]?.models ?? [];
44
49
  if (!providerModels.length) return undefined;
@@ -71,12 +76,12 @@ export function getFastModelForAuth(
71
76
  );
72
77
  if (!filteredModels.length) return getFastModel(provider);
73
78
 
74
- if (authType !== 'oauth') {
75
- const preferred = PREFERRED_FAST_MODELS[provider] ?? [];
76
- for (const modelId of preferred) {
77
- if (filteredModels.some((m) => m.id === modelId)) {
78
- return modelId;
79
- }
79
+ const preferredMap =
80
+ authType === 'oauth' ? PREFERRED_FAST_MODELS_OAUTH : PREFERRED_FAST_MODELS;
81
+ const preferred = preferredMap[provider] ?? [];
82
+ for (const modelId of preferred) {
83
+ if (filteredModels.some((m) => m.id === modelId)) {
84
+ return modelId;
80
85
  }
81
86
  }
82
87
 
package/src/tunnel/qr.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-expect-error No bundled types for qrcode-terminal in all workspace builds
1
2
  import qrcode from 'qrcode-terminal';
2
3
 
3
4
  export function generateQRCode(data: string): Promise<string> {
@@ -0,0 +1,14 @@
1
+ declare module '*.dylib' {
2
+ const filePath: string;
3
+ export default filePath;
4
+ }
5
+
6
+ declare module '*.so' {
7
+ const filePath: string;
8
+ export default filePath;
9
+ }
10
+
11
+ declare module '*.dll' {
12
+ const filePath: string;
13
+ export default filePath;
14
+ }
@@ -1,3 +1,5 @@
1
+ import type { ProviderId } from './provider';
2
+
1
3
  /**
2
4
  * Configuration scope - where settings are stored
3
5
  */
@@ -18,6 +20,14 @@ export type DefaultConfig = {
18
20
  theme?: string;
19
21
  };
20
22
 
23
+ export type ProviderSettings = Record<
24
+ ProviderId,
25
+ {
26
+ enabled: boolean;
27
+ apiKey?: string;
28
+ }
29
+ >;
30
+
21
31
  /**
22
32
  * Path configuration
23
33
  */
@@ -34,6 +44,7 @@ export type PathConfig = {
34
44
  export type OttoConfig = {
35
45
  projectRoot: string;
36
46
  defaults: DefaultConfig;
47
+ providers: ProviderSettings;
37
48
  paths: PathConfig;
38
49
  onboardingComplete?: boolean;
39
50
  };
@@ -16,6 +16,7 @@ export type {
16
16
  Scope,
17
17
  DefaultConfig,
18
18
  PathConfig,
19
+ ProviderSettings,
19
20
  OttoConfig,
20
21
  ToolApprovalMode,
21
22
  } from './config';