@ottocode/server 0.1.225 → 0.1.226
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 +3 -3
- package/src/openapi/paths/auth.ts +190 -0
- package/src/routes/auth.ts +388 -4
- package/src/routes/config/models.ts +77 -2
- package/src/runtime/agent/oauth-codex-continuation.ts +10 -0
- package/src/runtime/agent/runner-setup.ts +7 -0
- package/src/runtime/agent/runner.ts +16 -4
- package/src/runtime/provider/copilot.ts +119 -8
- package/src/runtime/provider/oauth-adapter.ts +2 -3
- package/src/runtime/utils/token.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.226",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/sdk": "0.1.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.226",
|
|
53
|
+
"@ottocode/database": "0.1.226",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.3.6"
|
|
@@ -35,6 +35,8 @@ export const authPaths = {
|
|
|
35
35
|
},
|
|
36
36
|
label: { type: 'string' },
|
|
37
37
|
supportsOAuth: { type: 'boolean' },
|
|
38
|
+
supportsToken: { type: 'boolean' },
|
|
39
|
+
supportsGhImport: { type: 'boolean' },
|
|
38
40
|
modelCount: { type: 'integer' },
|
|
39
41
|
costRange: {
|
|
40
42
|
type: 'object',
|
|
@@ -473,6 +475,194 @@ export const authPaths = {
|
|
|
473
475
|
},
|
|
474
476
|
},
|
|
475
477
|
},
|
|
478
|
+
'/v1/auth/copilot/methods': {
|
|
479
|
+
get: {
|
|
480
|
+
tags: ['auth'],
|
|
481
|
+
operationId: 'getCopilotAuthMethods',
|
|
482
|
+
summary: 'Get available Copilot auth methods',
|
|
483
|
+
responses: {
|
|
484
|
+
200: {
|
|
485
|
+
description: 'OK',
|
|
486
|
+
content: {
|
|
487
|
+
'application/json': {
|
|
488
|
+
schema: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {
|
|
491
|
+
oauth: { type: 'boolean' },
|
|
492
|
+
token: { type: 'boolean' },
|
|
493
|
+
ghImport: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {
|
|
496
|
+
available: { type: 'boolean' },
|
|
497
|
+
authenticated: { type: 'boolean' },
|
|
498
|
+
reason: { type: 'string' },
|
|
499
|
+
},
|
|
500
|
+
required: ['available', 'authenticated'],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
required: ['oauth', 'token', 'ghImport'],
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
'/v1/auth/copilot/token': {
|
|
512
|
+
post: {
|
|
513
|
+
tags: ['auth'],
|
|
514
|
+
operationId: 'saveCopilotToken',
|
|
515
|
+
summary: 'Save Copilot token after validating model access',
|
|
516
|
+
requestBody: {
|
|
517
|
+
required: true,
|
|
518
|
+
content: {
|
|
519
|
+
'application/json': {
|
|
520
|
+
schema: {
|
|
521
|
+
type: 'object',
|
|
522
|
+
properties: {
|
|
523
|
+
token: { type: 'string' },
|
|
524
|
+
},
|
|
525
|
+
required: ['token'],
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
responses: {
|
|
531
|
+
200: {
|
|
532
|
+
description: 'OK',
|
|
533
|
+
content: {
|
|
534
|
+
'application/json': {
|
|
535
|
+
schema: {
|
|
536
|
+
type: 'object',
|
|
537
|
+
properties: {
|
|
538
|
+
success: { type: 'boolean' },
|
|
539
|
+
provider: { type: 'string' },
|
|
540
|
+
source: { type: 'string', enum: ['token'] },
|
|
541
|
+
modelCount: { type: 'integer' },
|
|
542
|
+
hasGpt52Codex: { type: 'boolean' },
|
|
543
|
+
sampleModels: {
|
|
544
|
+
type: 'array',
|
|
545
|
+
items: { type: 'string' },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
required: [
|
|
549
|
+
'success',
|
|
550
|
+
'provider',
|
|
551
|
+
'source',
|
|
552
|
+
'modelCount',
|
|
553
|
+
'hasGpt52Codex',
|
|
554
|
+
'sampleModels',
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
400: errorResponse(),
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
'/v1/auth/copilot/gh/import': {
|
|
565
|
+
post: {
|
|
566
|
+
tags: ['auth'],
|
|
567
|
+
operationId: 'importCopilotTokenFromGh',
|
|
568
|
+
summary: 'Import Copilot token from GitHub CLI (gh)',
|
|
569
|
+
responses: {
|
|
570
|
+
200: {
|
|
571
|
+
description: 'OK',
|
|
572
|
+
content: {
|
|
573
|
+
'application/json': {
|
|
574
|
+
schema: {
|
|
575
|
+
type: 'object',
|
|
576
|
+
properties: {
|
|
577
|
+
success: { type: 'boolean' },
|
|
578
|
+
provider: { type: 'string' },
|
|
579
|
+
source: { type: 'string', enum: ['gh'] },
|
|
580
|
+
modelCount: { type: 'integer' },
|
|
581
|
+
hasGpt52Codex: { type: 'boolean' },
|
|
582
|
+
sampleModels: {
|
|
583
|
+
type: 'array',
|
|
584
|
+
items: { type: 'string' },
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: [
|
|
588
|
+
'success',
|
|
589
|
+
'provider',
|
|
590
|
+
'source',
|
|
591
|
+
'modelCount',
|
|
592
|
+
'hasGpt52Codex',
|
|
593
|
+
'sampleModels',
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
400: errorResponse(),
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
'/v1/auth/copilot/diagnostics': {
|
|
604
|
+
get: {
|
|
605
|
+
tags: ['auth'],
|
|
606
|
+
operationId: 'getCopilotDiagnostics',
|
|
607
|
+
summary: 'Get Copilot token diagnostics and model visibility',
|
|
608
|
+
responses: {
|
|
609
|
+
200: {
|
|
610
|
+
description: 'OK',
|
|
611
|
+
content: {
|
|
612
|
+
'application/json': {
|
|
613
|
+
schema: {
|
|
614
|
+
type: 'object',
|
|
615
|
+
properties: {
|
|
616
|
+
tokenSources: {
|
|
617
|
+
type: 'array',
|
|
618
|
+
items: {
|
|
619
|
+
type: 'object',
|
|
620
|
+
properties: {
|
|
621
|
+
source: {
|
|
622
|
+
type: 'string',
|
|
623
|
+
enum: ['env', 'stored'],
|
|
624
|
+
},
|
|
625
|
+
configured: { type: 'boolean' },
|
|
626
|
+
modelCount: { type: 'integer' },
|
|
627
|
+
hasGpt52Codex: { type: 'boolean' },
|
|
628
|
+
sampleModels: {
|
|
629
|
+
type: 'array',
|
|
630
|
+
items: { type: 'string' },
|
|
631
|
+
},
|
|
632
|
+
restrictedByOrgPolicy: { type: 'boolean' },
|
|
633
|
+
restrictedOrg: { type: 'string' },
|
|
634
|
+
restrictionMessage: { type: 'string' },
|
|
635
|
+
error: { type: 'string' },
|
|
636
|
+
},
|
|
637
|
+
required: ['source', 'configured'],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
methods: {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
oauth: { type: 'boolean' },
|
|
644
|
+
token: { type: 'boolean' },
|
|
645
|
+
ghImport: {
|
|
646
|
+
type: 'object',
|
|
647
|
+
properties: {
|
|
648
|
+
available: { type: 'boolean' },
|
|
649
|
+
authenticated: { type: 'boolean' },
|
|
650
|
+
reason: { type: 'string' },
|
|
651
|
+
},
|
|
652
|
+
required: ['available', 'authenticated'],
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
required: ['oauth', 'token', 'ghImport'],
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
required: ['tokenSources', 'methods'],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
476
666
|
'/v1/auth/onboarding/complete': {
|
|
477
667
|
post: {
|
|
478
668
|
tags: ['auth'],
|
package/src/routes/auth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import {
|
|
3
3
|
getAllAuth,
|
|
4
|
+
getAuth,
|
|
4
5
|
setAuth,
|
|
5
6
|
removeAuth,
|
|
6
7
|
ensureSetuWallet,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
importWallet,
|
|
9
10
|
loadConfig,
|
|
10
11
|
catalog,
|
|
12
|
+
readEnvKey,
|
|
11
13
|
getOnboardingComplete,
|
|
12
14
|
setOnboardingComplete,
|
|
13
15
|
authorize,
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
pollForCopilotTokenOnce,
|
|
21
23
|
type ProviderId,
|
|
22
24
|
} from '@ottocode/sdk';
|
|
25
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
23
26
|
import { logger } from '@ottocode/sdk';
|
|
24
27
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
25
28
|
|
|
@@ -33,6 +36,172 @@ const copilotDeviceSessions = new Map<
|
|
|
33
36
|
{ deviceCode: string; interval: number; provider: string; createdAt: number }
|
|
34
37
|
>();
|
|
35
38
|
|
|
39
|
+
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
|
|
40
|
+
const GH_CAPABILITY_CACHE_TTL_MS = 60 * 1000;
|
|
41
|
+
|
|
42
|
+
let ghCapabilityCache: {
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
value: { available: boolean; authenticated: boolean; reason?: string };
|
|
45
|
+
} = {
|
|
46
|
+
expiresAt: 0,
|
|
47
|
+
value: {
|
|
48
|
+
available: false,
|
|
49
|
+
authenticated: false,
|
|
50
|
+
reason: 'Not checked yet',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function getGhImportCapability() {
|
|
55
|
+
if (ghCapabilityCache.expiresAt > Date.now()) return ghCapabilityCache.value;
|
|
56
|
+
|
|
57
|
+
const version = spawnSync('gh', ['--version'], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
if (version.status !== 0) {
|
|
62
|
+
ghCapabilityCache = {
|
|
63
|
+
expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
|
|
64
|
+
value: {
|
|
65
|
+
available: false,
|
|
66
|
+
authenticated: false,
|
|
67
|
+
reason: 'GitHub CLI (gh) is not installed',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return ghCapabilityCache.value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const authStatus = spawnSync('gh', ['auth', 'status', '-h', 'github.com'], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
+
});
|
|
77
|
+
if (authStatus.status !== 0) {
|
|
78
|
+
ghCapabilityCache = {
|
|
79
|
+
expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
|
|
80
|
+
value: {
|
|
81
|
+
available: true,
|
|
82
|
+
authenticated: false,
|
|
83
|
+
reason: 'Run `gh auth login` first',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return ghCapabilityCache.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ghCapabilityCache = {
|
|
90
|
+
expiresAt: Date.now() + GH_CAPABILITY_CACHE_TTL_MS,
|
|
91
|
+
value: {
|
|
92
|
+
available: true,
|
|
93
|
+
authenticated: true,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return ghCapabilityCache.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseErrorMessageFromBody(text: string): string | undefined {
|
|
100
|
+
if (!text) return undefined;
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(text) as {
|
|
103
|
+
message?: string;
|
|
104
|
+
error?: { message?: string };
|
|
105
|
+
};
|
|
106
|
+
return parsed.error?.message ?? parsed.message;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function fetchCopilotModels(token: string): Promise<
|
|
113
|
+
| {
|
|
114
|
+
ok: true;
|
|
115
|
+
models: Set<string>;
|
|
116
|
+
}
|
|
117
|
+
| {
|
|
118
|
+
ok: false;
|
|
119
|
+
status: number;
|
|
120
|
+
message: string;
|
|
121
|
+
}
|
|
122
|
+
> {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(COPILOT_MODELS_URL, {
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${token}`,
|
|
127
|
+
'Openai-Intent': 'conversation-edits',
|
|
128
|
+
'User-Agent': 'ottocode',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const text = await response.text();
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
status: response.status,
|
|
136
|
+
message:
|
|
137
|
+
parseErrorMessageFromBody(text) ||
|
|
138
|
+
`Copilot models endpoint returned ${response.status}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const payload = JSON.parse(text) as {
|
|
143
|
+
data?: Array<{ id?: string }>;
|
|
144
|
+
};
|
|
145
|
+
const models = new Set(
|
|
146
|
+
(payload.data ?? [])
|
|
147
|
+
.map((item) => item.id)
|
|
148
|
+
.filter((id): id is string => Boolean(id)),
|
|
149
|
+
);
|
|
150
|
+
return { ok: true, models };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message =
|
|
153
|
+
error instanceof Error ? error.message : 'Failed to fetch Copilot models';
|
|
154
|
+
return { ok: false, status: 0, message };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function detectOAuthOrgRestriction(token: string): Promise<{
|
|
159
|
+
restricted: boolean;
|
|
160
|
+
org?: string;
|
|
161
|
+
message?: string;
|
|
162
|
+
}> {
|
|
163
|
+
try {
|
|
164
|
+
const orgsResponse = await fetch('https://api.github.com/user/orgs', {
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: `Bearer ${token}`,
|
|
167
|
+
'User-Agent': 'ottocode',
|
|
168
|
+
Accept: 'application/vnd.github+json',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (!orgsResponse.ok) {
|
|
172
|
+
return { restricted: false };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const orgs = (await orgsResponse.json()) as Array<{ login?: string }>;
|
|
176
|
+
for (const org of orgs) {
|
|
177
|
+
if (!org.login) continue;
|
|
178
|
+
const membershipResponse = await fetch(
|
|
179
|
+
`https://api.github.com/user/memberships/orgs/${org.login}`,
|
|
180
|
+
{
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Bearer ${token}`,
|
|
183
|
+
'User-Agent': 'ottocode',
|
|
184
|
+
Accept: 'application/vnd.github+json',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
if (membershipResponse.status !== 403) continue;
|
|
189
|
+
|
|
190
|
+
const bodyText = await membershipResponse.text();
|
|
191
|
+
const message = parseErrorMessageFromBody(bodyText) || bodyText;
|
|
192
|
+
if (message.includes('enabled OAuth App access restrictions')) {
|
|
193
|
+
return {
|
|
194
|
+
restricted: true,
|
|
195
|
+
org: org.login,
|
|
196
|
+
message,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {}
|
|
201
|
+
|
|
202
|
+
return { restricted: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
36
205
|
setInterval(() => {
|
|
37
206
|
const now = Date.now();
|
|
38
207
|
for (const [key, value] of oauthVerifiers.entries()) {
|
|
@@ -55,6 +224,7 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
55
224
|
const cfg = await loadConfig(projectRoot);
|
|
56
225
|
const onboardingComplete = await getOnboardingComplete(projectRoot);
|
|
57
226
|
const setuWallet = await getSetuWallet(projectRoot);
|
|
227
|
+
const ghImportCapability = getGhImportCapability();
|
|
58
228
|
|
|
59
229
|
const providers: Record<
|
|
60
230
|
string,
|
|
@@ -63,6 +233,8 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
63
233
|
type?: 'api' | 'oauth' | 'wallet';
|
|
64
234
|
label: string;
|
|
65
235
|
supportsOAuth: boolean;
|
|
236
|
+
supportsToken?: boolean;
|
|
237
|
+
supportsGhImport?: boolean;
|
|
66
238
|
modelCount: number;
|
|
67
239
|
costRange?: { min: number; max: number };
|
|
68
240
|
}
|
|
@@ -81,6 +253,9 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
81
253
|
label: entry.label || id,
|
|
82
254
|
supportsOAuth:
|
|
83
255
|
id === 'anthropic' || id === 'openai' || id === 'copilot',
|
|
256
|
+
supportsToken: id === 'copilot',
|
|
257
|
+
supportsGhImport:
|
|
258
|
+
id === 'copilot' ? ghImportCapability.available : false,
|
|
84
259
|
modelCount: models.length,
|
|
85
260
|
costRange:
|
|
86
261
|
costs.length > 0
|
|
@@ -213,15 +388,15 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
213
388
|
app.post('/v1/auth/:provider/oauth/url', async (c) => {
|
|
214
389
|
try {
|
|
215
390
|
const provider = c.req.param('provider');
|
|
216
|
-
const { mode
|
|
217
|
-
|
|
218
|
-
|
|
391
|
+
const body = await c.req.json<{ mode?: string }>().catch(() => undefined);
|
|
392
|
+
const mode: 'max' | 'console' =
|
|
393
|
+
body?.mode === 'console' ? 'console' : 'max';
|
|
219
394
|
|
|
220
395
|
let url: string;
|
|
221
396
|
let verifier: string;
|
|
222
397
|
|
|
223
398
|
if (provider === 'anthropic') {
|
|
224
|
-
const result = await authorize(mode
|
|
399
|
+
const result = await authorize(mode);
|
|
225
400
|
url = result.url;
|
|
226
401
|
verifier = result.verifier;
|
|
227
402
|
} else if (provider === 'openai') {
|
|
@@ -585,6 +760,215 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
585
760
|
}
|
|
586
761
|
});
|
|
587
762
|
|
|
763
|
+
app.get('/v1/auth/copilot/methods', async (c) => {
|
|
764
|
+
const ghImport = getGhImportCapability();
|
|
765
|
+
return c.json({
|
|
766
|
+
oauth: true,
|
|
767
|
+
token: true,
|
|
768
|
+
ghImport,
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
app.post('/v1/auth/copilot/token', async (c) => {
|
|
773
|
+
try {
|
|
774
|
+
const { token } = await c.req.json<{ token: string }>();
|
|
775
|
+
const sanitized = token?.trim();
|
|
776
|
+
if (!sanitized) {
|
|
777
|
+
return c.json({ error: 'Copilot token is required' }, 400);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const modelsResult = await fetchCopilotModels(sanitized);
|
|
781
|
+
if (!modelsResult.ok) {
|
|
782
|
+
return c.json(
|
|
783
|
+
{
|
|
784
|
+
error: `Invalid Copilot token: ${modelsResult.message}`,
|
|
785
|
+
},
|
|
786
|
+
400,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
await setAuth(
|
|
791
|
+
'copilot',
|
|
792
|
+
{
|
|
793
|
+
type: 'oauth',
|
|
794
|
+
refresh: sanitized,
|
|
795
|
+
access: sanitized,
|
|
796
|
+
expires: 0,
|
|
797
|
+
},
|
|
798
|
+
undefined,
|
|
799
|
+
'global',
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
const models = Array.from(modelsResult.models).sort();
|
|
803
|
+
return c.json({
|
|
804
|
+
success: true,
|
|
805
|
+
provider: 'copilot',
|
|
806
|
+
source: 'token',
|
|
807
|
+
modelCount: models.length,
|
|
808
|
+
hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
|
|
809
|
+
sampleModels: models.slice(0, 25),
|
|
810
|
+
});
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const message =
|
|
813
|
+
error instanceof Error ? error.message : 'Failed to save Copilot token';
|
|
814
|
+
logger.error('Failed to save Copilot token', error);
|
|
815
|
+
return c.json({ error: message }, 500);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
app.post('/v1/auth/copilot/gh/import', async (c) => {
|
|
820
|
+
try {
|
|
821
|
+
const ghImport = getGhImportCapability();
|
|
822
|
+
if (!ghImport.available) {
|
|
823
|
+
return c.json(
|
|
824
|
+
{
|
|
825
|
+
error: ghImport.reason || 'GitHub CLI is not available',
|
|
826
|
+
},
|
|
827
|
+
400,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (!ghImport.authenticated) {
|
|
831
|
+
return c.json(
|
|
832
|
+
{
|
|
833
|
+
error: ghImport.reason || 'GitHub CLI is not authenticated',
|
|
834
|
+
},
|
|
835
|
+
400,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const ghToken = execFileSync('gh', ['auth', 'token'], {
|
|
840
|
+
encoding: 'utf8',
|
|
841
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
842
|
+
}).trim();
|
|
843
|
+
if (!ghToken) {
|
|
844
|
+
return c.json({ error: 'GitHub CLI returned an empty token' }, 400);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const modelsResult = await fetchCopilotModels(ghToken);
|
|
848
|
+
if (!modelsResult.ok) {
|
|
849
|
+
return c.json(
|
|
850
|
+
{
|
|
851
|
+
error: `Imported gh token is not valid for Copilot: ${modelsResult.message}`,
|
|
852
|
+
},
|
|
853
|
+
400,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
await setAuth(
|
|
858
|
+
'copilot',
|
|
859
|
+
{
|
|
860
|
+
type: 'oauth',
|
|
861
|
+
refresh: ghToken,
|
|
862
|
+
access: ghToken,
|
|
863
|
+
expires: 0,
|
|
864
|
+
},
|
|
865
|
+
undefined,
|
|
866
|
+
'global',
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const models = Array.from(modelsResult.models).sort();
|
|
870
|
+
return c.json({
|
|
871
|
+
success: true,
|
|
872
|
+
provider: 'copilot',
|
|
873
|
+
source: 'gh',
|
|
874
|
+
modelCount: models.length,
|
|
875
|
+
hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
|
|
876
|
+
sampleModels: models.slice(0, 25),
|
|
877
|
+
});
|
|
878
|
+
} catch (error) {
|
|
879
|
+
const message =
|
|
880
|
+
error instanceof Error
|
|
881
|
+
? error.message
|
|
882
|
+
: 'Failed to import GitHub CLI token';
|
|
883
|
+
logger.error('Failed to import Copilot token from GitHub CLI', error);
|
|
884
|
+
return c.json({ error: message }, 500);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
app.get('/v1/auth/copilot/diagnostics', async (c) => {
|
|
889
|
+
try {
|
|
890
|
+
const projectRoot = process.cwd();
|
|
891
|
+
const entries: Array<{
|
|
892
|
+
source: 'env' | 'stored';
|
|
893
|
+
configured: boolean;
|
|
894
|
+
modelCount?: number;
|
|
895
|
+
hasGpt52Codex?: boolean;
|
|
896
|
+
sampleModels?: string[];
|
|
897
|
+
restrictedByOrgPolicy?: boolean;
|
|
898
|
+
restrictedOrg?: string;
|
|
899
|
+
restrictionMessage?: string;
|
|
900
|
+
error?: string;
|
|
901
|
+
}> = [];
|
|
902
|
+
|
|
903
|
+
const envToken = readEnvKey('copilot');
|
|
904
|
+
if (envToken) {
|
|
905
|
+
const modelsResult = await fetchCopilotModels(envToken);
|
|
906
|
+
if (modelsResult.ok) {
|
|
907
|
+
const models = Array.from(modelsResult.models).sort();
|
|
908
|
+
entries.push({
|
|
909
|
+
source: 'env',
|
|
910
|
+
configured: true,
|
|
911
|
+
modelCount: models.length,
|
|
912
|
+
hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
|
|
913
|
+
sampleModels: models.slice(0, 25),
|
|
914
|
+
});
|
|
915
|
+
} else {
|
|
916
|
+
entries.push({
|
|
917
|
+
source: 'env',
|
|
918
|
+
configured: true,
|
|
919
|
+
error: modelsResult.message,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
entries.push({ source: 'env', configured: false });
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const storedAuth = await getAuth('copilot', projectRoot);
|
|
927
|
+
if (storedAuth?.type === 'oauth') {
|
|
928
|
+
const modelsResult = await fetchCopilotModels(storedAuth.refresh);
|
|
929
|
+
const restriction = await detectOAuthOrgRestriction(storedAuth.refresh);
|
|
930
|
+
if (modelsResult.ok) {
|
|
931
|
+
const models = Array.from(modelsResult.models).sort();
|
|
932
|
+
entries.push({
|
|
933
|
+
source: 'stored',
|
|
934
|
+
configured: true,
|
|
935
|
+
modelCount: models.length,
|
|
936
|
+
hasGpt52Codex: modelsResult.models.has('gpt-5.2-codex'),
|
|
937
|
+
sampleModels: models.slice(0, 25),
|
|
938
|
+
restrictedByOrgPolicy: restriction.restricted,
|
|
939
|
+
restrictedOrg: restriction.org,
|
|
940
|
+
restrictionMessage: restriction.message,
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
entries.push({
|
|
944
|
+
source: 'stored',
|
|
945
|
+
configured: true,
|
|
946
|
+
error: modelsResult.message,
|
|
947
|
+
restrictedByOrgPolicy: restriction.restricted,
|
|
948
|
+
restrictedOrg: restriction.org,
|
|
949
|
+
restrictionMessage: restriction.message,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
} else {
|
|
953
|
+
entries.push({ source: 'stored', configured: false });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return c.json({
|
|
957
|
+
tokenSources: entries,
|
|
958
|
+
methods: {
|
|
959
|
+
oauth: true,
|
|
960
|
+
token: true,
|
|
961
|
+
ghImport: getGhImportCapability(),
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
} catch (error) {
|
|
965
|
+
const message =
|
|
966
|
+
error instanceof Error ? error.message : 'Failed to inspect Copilot';
|
|
967
|
+
logger.error('Failed to build Copilot diagnostics', error);
|
|
968
|
+
return c.json({ error: message }, 500);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
588
972
|
app.post('/v1/auth/onboarding/complete', async (c) => {
|
|
589
973
|
try {
|
|
590
974
|
await setOnboardingComplete();
|
|
@@ -2,11 +2,13 @@ import type { Hono } from 'hono';
|
|
|
2
2
|
import {
|
|
3
3
|
loadConfig,
|
|
4
4
|
catalog,
|
|
5
|
+
getAuth,
|
|
6
|
+
logger,
|
|
7
|
+
readEnvKey,
|
|
5
8
|
type ProviderId,
|
|
6
9
|
filterModelsForAuthType,
|
|
7
10
|
} from '@ottocode/sdk';
|
|
8
11
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
9
|
-
import { logger } from '@ottocode/sdk';
|
|
10
12
|
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
11
13
|
import {
|
|
12
14
|
isProviderAuthorizedHybrid,
|
|
@@ -15,6 +17,68 @@ import {
|
|
|
15
17
|
getAuthTypeForProvider,
|
|
16
18
|
} from './utils.ts';
|
|
17
19
|
|
|
20
|
+
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
|
|
21
|
+
|
|
22
|
+
function filterCopilotAvailability<T extends { id: string }>(
|
|
23
|
+
provider: ProviderId,
|
|
24
|
+
models: T[],
|
|
25
|
+
copilotAllowedModels: Set<string> | null,
|
|
26
|
+
): T[] {
|
|
27
|
+
if (provider !== 'copilot') return models;
|
|
28
|
+
if (!copilotAllowedModels || copilotAllowedModels.size === 0) return models;
|
|
29
|
+
return models.filter((m) => copilotAllowedModels.has(m.id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getCopilotAuthTokens(projectRoot: string): Promise<string[]> {
|
|
33
|
+
const tokens: string[] = [];
|
|
34
|
+
|
|
35
|
+
const envToken = readEnvKey('copilot');
|
|
36
|
+
if (envToken) tokens.push(envToken);
|
|
37
|
+
|
|
38
|
+
const auth = await getAuth('copilot', projectRoot);
|
|
39
|
+
if (auth?.type === 'oauth' && auth.refresh) {
|
|
40
|
+
if (auth.refresh !== envToken) {
|
|
41
|
+
tokens.push(auth.refresh);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return tokens;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getAuthorizedCopilotModels(
|
|
49
|
+
projectRoot: string,
|
|
50
|
+
): Promise<Set<string> | null> {
|
|
51
|
+
const tokens = await getCopilotAuthTokens(projectRoot);
|
|
52
|
+
if (!tokens.length) return null;
|
|
53
|
+
|
|
54
|
+
const merged = new Set<string>();
|
|
55
|
+
let successful = false;
|
|
56
|
+
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(COPILOT_MODELS_URL, {
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
'Openai-Intent': 'conversation-edits',
|
|
63
|
+
'User-Agent': 'ottocode',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) continue;
|
|
67
|
+
|
|
68
|
+
successful = true;
|
|
69
|
+
const payload = (await response.json()) as {
|
|
70
|
+
data?: Array<{ id?: string }>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const id of (payload.data ?? []).map((item) => item.id)) {
|
|
74
|
+
if (id) merged.add(id);
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return successful ? merged : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
18
82
|
export function registerModelsRoutes(app: Hono) {
|
|
19
83
|
app.get('/v1/config/providers/:provider/models', async (c) => {
|
|
20
84
|
try {
|
|
@@ -53,9 +117,19 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
53
117
|
providerCatalog.models,
|
|
54
118
|
authType,
|
|
55
119
|
);
|
|
120
|
+
const copilotAllowedModels =
|
|
121
|
+
provider === 'copilot'
|
|
122
|
+
? await getAuthorizedCopilotModels(projectRoot)
|
|
123
|
+
: null;
|
|
124
|
+
|
|
125
|
+
const availableModels = filterCopilotAvailability(
|
|
126
|
+
provider,
|
|
127
|
+
filteredModels,
|
|
128
|
+
copilotAllowedModels,
|
|
129
|
+
);
|
|
56
130
|
|
|
57
131
|
return c.json({
|
|
58
|
-
models:
|
|
132
|
+
models: availableModels.map((m) => ({
|
|
59
133
|
id: m.id,
|
|
60
134
|
label: m.label || m.id,
|
|
61
135
|
toolCall: m.toolCall,
|
|
@@ -94,6 +168,7 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
94
168
|
string,
|
|
95
169
|
{
|
|
96
170
|
label: string;
|
|
171
|
+
authType?: 'api' | 'oauth' | 'wallet';
|
|
97
172
|
models: Array<{
|
|
98
173
|
id: string;
|
|
99
174
|
label: string;
|
|
@@ -2,12 +2,14 @@ export type OauthCodexContinuationInput = {
|
|
|
2
2
|
provider: string;
|
|
3
3
|
isOpenAIOAuth: boolean;
|
|
4
4
|
finishObserved: boolean;
|
|
5
|
+
abortedByUser?: boolean;
|
|
5
6
|
continuationCount: number;
|
|
6
7
|
maxContinuations: number;
|
|
7
8
|
finishReason?: string;
|
|
8
9
|
rawFinishReason?: string;
|
|
9
10
|
firstToolSeen: boolean;
|
|
10
11
|
hasTrailingAssistantText: boolean;
|
|
12
|
+
endedWithToolActivity?: boolean;
|
|
11
13
|
droppedPseudoToolText: boolean;
|
|
12
14
|
lastAssistantText: string;
|
|
13
15
|
};
|
|
@@ -68,6 +70,10 @@ export function decideOauthCodexContinuation(
|
|
|
68
70
|
return { shouldContinue: false };
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
if (input.abortedByUser) {
|
|
74
|
+
return { shouldContinue: false, reason: 'aborted-by-user' };
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
if (input.continuationCount >= input.maxContinuations) {
|
|
72
78
|
return { shouldContinue: false, reason: 'max-continuations-reached' };
|
|
73
79
|
}
|
|
@@ -76,6 +82,10 @@ export function decideOauthCodexContinuation(
|
|
|
76
82
|
return { shouldContinue: true, reason: 'truncated' };
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
if (input.endedWithToolActivity) {
|
|
86
|
+
return { shouldContinue: true, reason: 'ended-on-tool-activity' };
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
if (isMissingAssistantSummary(input)) {
|
|
80
90
|
return { shouldContinue: true, reason: 'no-trailing-assistant-text' };
|
|
81
91
|
}
|
|
@@ -211,6 +211,13 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
211
211
|
const providerOptions = { ...adapted.providerOptions };
|
|
212
212
|
let effectiveMaxOutputTokens = maxOutputTokens;
|
|
213
213
|
|
|
214
|
+
if (opts.provider === 'copilot') {
|
|
215
|
+
providerOptions.openai = {
|
|
216
|
+
...((providerOptions.openai as Record<string, unknown>) || {}),
|
|
217
|
+
store: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
214
221
|
if (opts.reasoningText) {
|
|
215
222
|
const underlyingProvider = getUnderlyingProviderKey(
|
|
216
223
|
opts.provider,
|
|
@@ -182,6 +182,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
182
182
|
let _finishObserved = false;
|
|
183
183
|
let _toolActivityObserved = false;
|
|
184
184
|
let _trailingAssistantTextAfterTool = false;
|
|
185
|
+
let _abortedByUser = false;
|
|
185
186
|
const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
|
|
186
187
|
if (evt.type === 'tool.call' || evt.type === 'tool.result') {
|
|
187
188
|
_toolActivityObserved = true;
|
|
@@ -245,12 +246,19 @@ async function runAssistant(opts: RunOpts) {
|
|
|
245
246
|
runSessionLoop,
|
|
246
247
|
);
|
|
247
248
|
|
|
248
|
-
const
|
|
249
|
+
const baseOnAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
250
|
+
const onAbort = async (event: Parameters<typeof baseOnAbort>[0]) => {
|
|
251
|
+
_abortedByUser = true;
|
|
252
|
+
await baseOnAbort(event);
|
|
253
|
+
};
|
|
249
254
|
|
|
250
255
|
const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
const isCopilotResponsesApi =
|
|
257
|
+
opts.provider === 'copilot' && !opts.model.startsWith('gpt-5-mini');
|
|
258
|
+
const stopWhenCondition =
|
|
259
|
+
isOpenAIOAuth || isCopilotResponsesApi
|
|
260
|
+
? stepCountIs(20)
|
|
261
|
+
: hasToolCall('finish');
|
|
254
262
|
|
|
255
263
|
try {
|
|
256
264
|
const result = streamText({
|
|
@@ -421,16 +429,20 @@ async function runAssistant(opts: RunOpts) {
|
|
|
421
429
|
|
|
422
430
|
const MAX_CONTINUATIONS = 6;
|
|
423
431
|
const continuationCount = opts.continuationCount ?? 0;
|
|
432
|
+
const endedWithToolActivity =
|
|
433
|
+
_toolActivityObserved && !_trailingAssistantTextAfterTool;
|
|
424
434
|
const continuationDecision = decideOauthCodexContinuation({
|
|
425
435
|
provider: opts.provider,
|
|
426
436
|
isOpenAIOAuth,
|
|
427
437
|
finishObserved: _finishObserved,
|
|
438
|
+
abortedByUser: _abortedByUser,
|
|
428
439
|
continuationCount,
|
|
429
440
|
maxContinuations: MAX_CONTINUATIONS,
|
|
430
441
|
finishReason: streamFinishReason,
|
|
431
442
|
rawFinishReason: streamRawFinishReason,
|
|
432
443
|
firstToolSeen: fs,
|
|
433
444
|
hasTrailingAssistantText: _trailingAssistantTextAfterTool,
|
|
445
|
+
endedWithToolActivity,
|
|
434
446
|
droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
|
|
435
447
|
lastAssistantText: latestAssistantText,
|
|
436
448
|
});
|
|
@@ -1,12 +1,123 @@
|
|
|
1
|
-
import { getAuth, createCopilotModel } from '@ottocode/sdk';
|
|
2
|
-
import type { OttoConfig } from '@ottocode/sdk';
|
|
1
|
+
import { getAuth, createCopilotModel, readEnvKey } from '@ottocode/sdk';
|
|
2
|
+
import type { OttoConfig, OAuth } from '@ottocode/sdk';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
|
|
5
|
+
const COPILOT_MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
type CachedCopilotModels = {
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
models: Set<string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const copilotModelsCache = new Map<string, CachedCopilotModels>();
|
|
13
|
+
|
|
14
|
+
type CopilotTokenCandidate = {
|
|
15
|
+
source: 'env' | 'oauth';
|
|
16
|
+
token: string;
|
|
17
|
+
oauth: OAuth;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function getCopilotTokenCandidates(
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
): Promise<CopilotTokenCandidate[]> {
|
|
23
|
+
const candidates: CopilotTokenCandidate[] = [];
|
|
24
|
+
|
|
25
|
+
const envToken = readEnvKey('copilot');
|
|
26
|
+
if (envToken) {
|
|
27
|
+
candidates.push({
|
|
28
|
+
source: 'env',
|
|
29
|
+
token: envToken,
|
|
30
|
+
oauth: {
|
|
31
|
+
type: 'oauth',
|
|
32
|
+
access: envToken,
|
|
33
|
+
refresh: envToken,
|
|
34
|
+
expires: 0,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const auth = await getAuth('copilot', projectRoot);
|
|
6
40
|
if (auth?.type === 'oauth') {
|
|
7
|
-
|
|
41
|
+
if (auth.refresh !== envToken) {
|
|
42
|
+
candidates.push({ source: 'oauth', token: auth.refresh, oauth: auth });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getCopilotAvailableModels(
|
|
50
|
+
token: string,
|
|
51
|
+
): Promise<Set<string> | null> {
|
|
52
|
+
const cached = copilotModelsCache.get(token);
|
|
53
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
54
|
+
return cached.models;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(COPILOT_MODELS_URL, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${token}`,
|
|
61
|
+
'Openai-Intent': 'conversation-edits',
|
|
62
|
+
'User-Agent': 'ottocode',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) return null;
|
|
67
|
+
|
|
68
|
+
const payload = (await response.json()) as {
|
|
69
|
+
data?: Array<{ id?: string }>;
|
|
70
|
+
};
|
|
71
|
+
const models = new Set(
|
|
72
|
+
(payload.data ?? [])
|
|
73
|
+
.map((item) => item.id)
|
|
74
|
+
.filter((id): id is string => Boolean(id)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
copilotModelsCache.set(token, {
|
|
78
|
+
expiresAt: Date.now() + COPILOT_MODELS_CACHE_TTL_MS,
|
|
79
|
+
models,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return models;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function resolveCopilotModel(model: string, cfg: OttoConfig) {
|
|
89
|
+
const candidates = await getCopilotTokenCandidates(cfg.projectRoot);
|
|
90
|
+
if (!candidates.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Copilot provider requires OAuth or GITHUB_TOKEN. Run `otto auth login copilot` or set GITHUB_TOKEN.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let selected: CopilotTokenCandidate | null = null;
|
|
97
|
+
const unionAvailableModels = new Set<string>();
|
|
98
|
+
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
const availableModels = await getCopilotAvailableModels(candidate.token);
|
|
101
|
+
if (!availableModels || availableModels.size === 0) continue;
|
|
102
|
+
|
|
103
|
+
for (const availableModel of availableModels) {
|
|
104
|
+
unionAvailableModels.add(availableModel);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!selected && availableModels.has(model)) {
|
|
108
|
+
selected = candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (selected) {
|
|
113
|
+
return createCopilotModel(model, { oauth: selected.oauth });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (unionAvailableModels.size > 0) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Copilot model '${model}' is not available for this account/organization token. Available models: ${Array.from(unionAvailableModels).join(', ')}`,
|
|
119
|
+
);
|
|
8
120
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
121
|
+
|
|
122
|
+
return createCopilotModel(model, { oauth: candidates[0].oauth });
|
|
12
123
|
}
|
|
@@ -69,11 +69,10 @@ export function detectOAuth(
|
|
|
69
69
|
): OAuthContext {
|
|
70
70
|
const isOAuth = auth?.type === 'oauth';
|
|
71
71
|
const needsSpoof = !!isOAuth && provider === 'anthropic';
|
|
72
|
-
const isCopilot = provider === 'copilot';
|
|
73
72
|
return {
|
|
74
|
-
isOAuth: !!isOAuth
|
|
73
|
+
isOAuth: !!isOAuth,
|
|
75
74
|
needsSpoof,
|
|
76
|
-
isOpenAIOAuth:
|
|
75
|
+
isOpenAIOAuth: !!isOAuth && provider === 'openai',
|
|
77
76
|
spoofPrompt: needsSpoof ? getProviderSpoofPrompt(provider) : undefined,
|
|
78
77
|
};
|
|
79
78
|
}
|