@ottocode/server 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 +3 -3
- package/src/events/types.ts +2 -0
- package/src/hono-context.d.ts +7 -0
- package/src/index.ts +9 -1
- package/src/openapi/paths/auth.ts +190 -0
- package/src/routes/ask.ts +5 -3
- package/src/routes/auth.ts +388 -4
- package/src/routes/config/agents.ts +5 -3
- package/src/routes/config/defaults.ts +3 -3
- package/src/routes/config/main.ts +5 -3
- package/src/routes/config/models.ts +87 -8
- package/src/routes/config/providers.ts +8 -4
- package/src/routes/config/utils.ts +11 -4
- package/src/routes/terminals.ts +6 -4
- package/src/routes/tunnel.ts +1 -1
- 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 +37 -11
- package/src/runtime/ask/service.ts +5 -0
- package/src/runtime/errors/api-error.ts +29 -21
- package/src/runtime/message/service.ts +2 -2
- package/src/runtime/provider/copilot.ts +119 -8
- package/src/runtime/provider/oauth-adapter.ts +2 -3
- package/src/runtime/session/branch.ts +11 -1
- package/src/runtime/session/manager.ts +6 -1
- package/src/runtime/stream/step-finish.ts +3 -0
- package/src/runtime/utils/token.ts +3 -0
- package/src/tools/adapter.ts +4 -3
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();
|
|
@@ -8,9 +8,11 @@ import { discoverAllAgents, getDefault } from './utils.ts';
|
|
|
8
8
|
export function registerAgentsRoute(app: Hono) {
|
|
9
9
|
app.get('/v1/config/agents', async (c) => {
|
|
10
10
|
try {
|
|
11
|
-
const embeddedConfig =
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const embeddedConfig = (
|
|
12
|
+
c as unknown as {
|
|
13
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
14
|
+
}
|
|
15
|
+
).get('embeddedConfig');
|
|
14
16
|
|
|
15
17
|
if (embeddedConfig) {
|
|
16
18
|
const agents = embeddedConfig.agents
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { setConfig, loadConfig } from '@ottocode/sdk';
|
|
2
|
+
import { setConfig, loadConfig, type ProviderId } from '@ottocode/sdk';
|
|
3
3
|
import { logger } from '@ottocode/sdk';
|
|
4
4
|
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
21
21
|
const scope = body.scope || 'global';
|
|
22
22
|
const updates: Partial<{
|
|
23
23
|
agent: string;
|
|
24
|
-
provider:
|
|
24
|
+
provider: ProviderId;
|
|
25
25
|
model: string;
|
|
26
26
|
toolApproval: 'auto' | 'dangerous' | 'all';
|
|
27
27
|
guidedMode: boolean;
|
|
@@ -30,7 +30,7 @@ export function registerDefaultsRoute(app: Hono) {
|
|
|
30
30
|
}> = {};
|
|
31
31
|
|
|
32
32
|
if (body.agent) updates.agent = body.agent;
|
|
33
|
-
if (body.provider) updates.provider = body.provider;
|
|
33
|
+
if (body.provider) updates.provider = body.provider as ProviderId;
|
|
34
34
|
if (body.model) updates.model = body.model;
|
|
35
35
|
if (body.toolApproval) updates.toolApproval = body.toolApproval;
|
|
36
36
|
if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
|
|
@@ -13,9 +13,11 @@ export function registerMainConfigRoute(app: Hono) {
|
|
|
13
13
|
app.get('/v1/config', async (c) => {
|
|
14
14
|
try {
|
|
15
15
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
16
|
-
const embeddedConfig =
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const embeddedConfig = (
|
|
17
|
+
c as unknown as {
|
|
18
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
19
|
+
}
|
|
20
|
+
).get('embeddedConfig');
|
|
19
21
|
|
|
20
22
|
const cfg = await loadConfig(projectRoot);
|
|
21
23
|
|
|
@@ -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,12 +17,76 @@ 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 {
|
|
21
|
-
const embeddedConfig =
|
|
22
|
-
|
|
23
|
-
|
|
85
|
+
const embeddedConfig = (
|
|
86
|
+
c as unknown as {
|
|
87
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
88
|
+
}
|
|
89
|
+
).get('embeddedConfig');
|
|
24
90
|
const provider = c.req.param('provider') as ProviderId;
|
|
25
91
|
|
|
26
92
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
@@ -53,9 +119,19 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
53
119
|
providerCatalog.models,
|
|
54
120
|
authType,
|
|
55
121
|
);
|
|
122
|
+
const copilotAllowedModels =
|
|
123
|
+
provider === 'copilot'
|
|
124
|
+
? await getAuthorizedCopilotModels(projectRoot)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
const availableModels = filterCopilotAvailability(
|
|
128
|
+
provider,
|
|
129
|
+
filteredModels,
|
|
130
|
+
copilotAllowedModels,
|
|
131
|
+
);
|
|
56
132
|
|
|
57
133
|
return c.json({
|
|
58
|
-
models:
|
|
134
|
+
models: availableModels.map((m) => ({
|
|
59
135
|
id: m.id,
|
|
60
136
|
label: m.label || m.id,
|
|
61
137
|
toolCall: m.toolCall,
|
|
@@ -78,9 +154,11 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
78
154
|
|
|
79
155
|
app.get('/v1/config/models', async (c) => {
|
|
80
156
|
try {
|
|
81
|
-
const embeddedConfig =
|
|
82
|
-
|
|
83
|
-
|
|
157
|
+
const embeddedConfig = (
|
|
158
|
+
c as unknown as {
|
|
159
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
160
|
+
}
|
|
161
|
+
).get('embeddedConfig');
|
|
84
162
|
|
|
85
163
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
86
164
|
const cfg = await loadConfig(projectRoot);
|
|
@@ -94,6 +172,7 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
94
172
|
string,
|
|
95
173
|
{
|
|
96
174
|
label: string;
|
|
175
|
+
authType?: 'api' | 'oauth' | 'wallet';
|
|
97
176
|
models: Array<{
|
|
98
177
|
id: string;
|
|
99
178
|
label: string;
|
|
@@ -9,14 +9,18 @@ import { getAuthorizedProviders, getDefault } from './utils.ts';
|
|
|
9
9
|
export function registerProvidersRoute(app: Hono) {
|
|
10
10
|
app.get('/v1/config/providers', async (c) => {
|
|
11
11
|
try {
|
|
12
|
-
const embeddedConfig =
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const embeddedConfig = (
|
|
13
|
+
c as unknown as {
|
|
14
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
15
|
+
}
|
|
16
|
+
).get('embeddedConfig');
|
|
15
17
|
|
|
16
18
|
if (embeddedConfig) {
|
|
17
19
|
const providers = embeddedConfig.auth
|
|
18
20
|
? (Object.keys(embeddedConfig.auth) as ProviderId[])
|
|
19
|
-
:
|
|
21
|
+
: embeddedConfig.provider
|
|
22
|
+
? [embeddedConfig.provider]
|
|
23
|
+
: [];
|
|
20
24
|
|
|
21
25
|
return c.json({
|
|
22
26
|
providers,
|