@playcraft/cli 0.1.84-beta → 0.1.85-beta
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/dist/commands/tools-generation.js +46 -102
- package/dist/commands/tools-utils.js +17 -15
- package/dist/utils/image-asset-context.js +42 -92
- package/dist/workspace-runtime/codex/capture-isolation.js +10 -0
- package/dist/workspace-runtime/codex/model-broker.js +269 -82
- package/dist/workspace-runtime/codex/model-capture.js +675 -0
- package/package.json +4 -3
- package/templates/preview-runtime/v1/manifest.json +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +1 -1
|
@@ -1,23 +1,41 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { Readable, Transform } from 'node:stream';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
5
|
import { basename, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { workspaceContextFromRoot } from '../workspaces/registry.js';
|
|
8
|
-
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
|
+
import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
9
|
import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-broker.js';
|
|
10
10
|
import { WorkspaceRemixReferencesBroker } from './remix-references-broker.js';
|
|
11
11
|
import { WorkspaceExecutionContextBroker } from './execution-context-broker.js';
|
|
12
12
|
import { WorkspaceSkillDiscoveryBroker } from './skill-discovery-broker.js';
|
|
13
|
+
import { WorkspaceModelCapture, captureSubmittedResponse, } from './model-capture.js';
|
|
13
14
|
const credentialSchema = z.object({
|
|
14
|
-
token: z.string().min(1),
|
|
15
|
-
|
|
15
|
+
token: z.string().min(1),
|
|
16
|
+
expiresAt: z.string().datetime(),
|
|
17
|
+
configId: z.string().uuid(),
|
|
18
|
+
model: z.string().min(1).max(128),
|
|
16
19
|
supportsReasoning: z.boolean().optional(),
|
|
17
20
|
claims: z.object({
|
|
18
|
-
environment: z.string(),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
environment: z.string(),
|
|
22
|
+
userId: z.string(),
|
|
23
|
+
workspaceId: z.string(),
|
|
24
|
+
runtimeId: z.string(),
|
|
25
|
+
generation: z.number().int(),
|
|
26
|
+
runtimeEpoch: z.number().int(),
|
|
27
|
+
traceOrigin: z
|
|
28
|
+
.custom((value) => workspaceExecutionTraceOriginSchema.safeParse(value).success)
|
|
29
|
+
.optional(),
|
|
30
|
+
capturePolicy: z
|
|
31
|
+
.object({
|
|
32
|
+
enabled: z.literal(true),
|
|
33
|
+
policyVersion: z.literal('workspace-model-capture-v1'),
|
|
34
|
+
maxRequestBytes: z.number().int().positive(),
|
|
35
|
+
maxResponseBytes: z.number().int().positive(),
|
|
36
|
+
expiresAt: z.string().datetime(),
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
21
39
|
}),
|
|
22
40
|
});
|
|
23
41
|
const MAX_MODEL_REQUEST_BYTES = 128 * 1024 * 1024;
|
|
@@ -35,12 +53,14 @@ export class WorkspaceModelBroker {
|
|
|
35
53
|
references;
|
|
36
54
|
executionContext;
|
|
37
55
|
skills;
|
|
56
|
+
capture;
|
|
38
57
|
constructor(state) {
|
|
39
58
|
this.state = state;
|
|
40
59
|
this.generation = new WorkspaceGenerationProvenanceBroker(state);
|
|
41
60
|
this.references = new WorkspaceRemixReferencesBroker(state);
|
|
42
61
|
this.executionContext = new WorkspaceExecutionContextBroker(state);
|
|
43
62
|
this.skills = new WorkspaceSkillDiscoveryBroker(state);
|
|
63
|
+
this.capture = new WorkspaceModelCapture(state);
|
|
44
64
|
}
|
|
45
65
|
async start() {
|
|
46
66
|
if (this.server)
|
|
@@ -57,6 +77,7 @@ export class WorkspaceModelBroker {
|
|
|
57
77
|
if (!address || typeof address === 'string')
|
|
58
78
|
throw new Error('model_broker_unavailable');
|
|
59
79
|
this.baseUrl = `http://127.0.0.1:${address.port}`;
|
|
80
|
+
await this.capture.start();
|
|
60
81
|
}
|
|
61
82
|
async threadConfig(cwd, modelConfigId, sessionId, reasoningEffort) {
|
|
62
83
|
const { activate, ...settings } = await this.prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort);
|
|
@@ -69,24 +90,44 @@ export class WorkspaceModelBroker {
|
|
|
69
90
|
const { workspaceId } = await workspaceContextFromRoot(this.state, cwd);
|
|
70
91
|
if (modelConfigId !== undefined && !z.string().uuid().safeParse(modelConfigId).success)
|
|
71
92
|
throw new Error('model_config_denied');
|
|
72
|
-
const routeKey = sessionId
|
|
93
|
+
const routeKey = sessionId
|
|
94
|
+
? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`
|
|
95
|
+
: workspaceId;
|
|
73
96
|
let route = this.routes.get(routeKey);
|
|
74
|
-
const selected = modelConfigId ??
|
|
97
|
+
const selected = modelConfigId ??
|
|
98
|
+
(route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
|
|
75
99
|
const cacheKey = `${workspaceId}:${selected ?? 'default'}:${reasoningEffort ?? 'default'}:${sessionId ?? ''}:${clientRequestId ?? ''}`;
|
|
76
|
-
let entry = this.entries.get(cacheKey) ??
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
let entry = this.entries.get(cacheKey) ??
|
|
101
|
+
[...this.entries.values()].find((item) => item.workspaceId === workspaceId &&
|
|
102
|
+
item.epoch === this.state.runtimeEpoch &&
|
|
103
|
+
item.modelConfigId === selected &&
|
|
104
|
+
item.sessionId === sessionId &&
|
|
105
|
+
item.clientRequestId === clientRequestId &&
|
|
106
|
+
item.reasoningEffort === reasoningEffort);
|
|
79
107
|
if (!entry || entry.epoch !== this.state.runtimeEpoch) {
|
|
80
|
-
entry = {
|
|
108
|
+
entry = {
|
|
109
|
+
workspaceId,
|
|
110
|
+
epoch: this.state.runtimeEpoch,
|
|
111
|
+
cacheKey,
|
|
112
|
+
modelConfigId: selected,
|
|
113
|
+
reasoningEffort,
|
|
114
|
+
sessionId,
|
|
115
|
+
clientRequestId,
|
|
116
|
+
};
|
|
81
117
|
this.entries.set(cacheKey, entry);
|
|
82
118
|
}
|
|
83
119
|
// Resolve current model metadata and permission at every native turn boundary.
|
|
84
120
|
const credential = await this.credential(entry, true);
|
|
85
121
|
const boundaryId = sessionId && clientRequestId ? await this.generation.prepare() : undefined;
|
|
86
122
|
if (!route || route.epoch !== this.state.runtimeEpoch) {
|
|
87
|
-
route = {
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
route = {
|
|
124
|
+
workspaceId,
|
|
125
|
+
epoch: this.state.runtimeEpoch,
|
|
126
|
+
capability: randomBytes(32).toString('base64url'),
|
|
127
|
+
generationCapability: randomBytes(32).toString('base64url'),
|
|
128
|
+
referencesCapability: randomBytes(32).toString('base64url'),
|
|
129
|
+
skillsCapability: randomBytes(32).toString('base64url'),
|
|
130
|
+
};
|
|
90
131
|
this.routes.set(routeKey, route);
|
|
91
132
|
}
|
|
92
133
|
// Each session pins its prepared model; shared credential renewal cannot change an active turn.
|
|
@@ -95,41 +136,67 @@ export class WorkspaceModelBroker {
|
|
|
95
136
|
if (!this.valid(entry) || this.routes.get(routeKey) !== route)
|
|
96
137
|
throw new Error('model_access_denied');
|
|
97
138
|
route.lastAdmissionReason = undefined;
|
|
98
|
-
route.selected = {
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
route.selected = {
|
|
140
|
+
entry,
|
|
141
|
+
model: credential.model,
|
|
142
|
+
...(sessionId && clientRequestId
|
|
143
|
+
? {
|
|
144
|
+
feedback: {
|
|
145
|
+
sessionId,
|
|
146
|
+
clientRequestId,
|
|
147
|
+
cwd,
|
|
148
|
+
boundaryId,
|
|
149
|
+
readOnly,
|
|
150
|
+
...(originCwd ? { originCwd } : {}),
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
101
155
|
},
|
|
102
156
|
model: credential.model,
|
|
103
157
|
modelConfigId: credential.configId,
|
|
104
158
|
modelProvider: 'playcraft_workspace',
|
|
105
159
|
config: {
|
|
106
|
-
...(sessionId
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
...(sessionId
|
|
161
|
+
? {
|
|
162
|
+
'shell_environment_policy.set': {
|
|
163
|
+
PLAYCRAFT_AGENT_GENERATION_ENDPOINT: `${this.baseUrl}/${routeKey}/generation-provenance`,
|
|
164
|
+
PLAYCRAFT_AGENT_GENERATION_CAPABILITY: route.generationCapability,
|
|
165
|
+
PLAYCRAFT_REMIX_REFERENCES_ENDPOINT: `${this.baseUrl}/${routeKey}/remix-references`,
|
|
166
|
+
PLAYCRAFT_REMIX_REFERENCES_CAPABILITY: route.referencesCapability,
|
|
167
|
+
PLAYCRAFT_SKILLS_ENDPOINT: `${this.baseUrl}/${routeKey}/skill-discovery`,
|
|
168
|
+
PLAYCRAFT_SKILLS_CAPABILITY: route.skillsCapability,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
: {}),
|
|
114
172
|
'model_providers.playcraft_workspace': {
|
|
115
|
-
name: 'PlayCraft Workspace',
|
|
116
|
-
|
|
173
|
+
name: 'PlayCraft Workspace',
|
|
174
|
+
base_url: `${this.baseUrl}/${routeKey}`,
|
|
175
|
+
wire_api: 'responses',
|
|
176
|
+
requires_openai_auth: false,
|
|
117
177
|
http_headers: { Authorization: `Bearer ${route.capability}` },
|
|
118
178
|
},
|
|
119
179
|
},
|
|
120
180
|
};
|
|
121
181
|
}
|
|
122
182
|
valid(entry) {
|
|
123
|
-
return Boolean(this.server &&
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
return Boolean(this.server &&
|
|
184
|
+
!this.state.journal.destroyed() &&
|
|
185
|
+
this.state.runtimeEpoch === entry.epoch &&
|
|
186
|
+
this.state.journal.getWorkspace(entry.workspaceId) &&
|
|
187
|
+
this.entries.get(entry.cacheKey) === entry);
|
|
126
188
|
}
|
|
127
189
|
/** Called immediately before the native turn/start write, within this broker's process clock. */
|
|
128
190
|
markCodexDispatched(cwd, sessionId, clientRequestId) {
|
|
129
|
-
const workspaceId = /^(basic|remix-)/.test(basename(cwd))
|
|
191
|
+
const workspaceId = /^(basic|remix-)/.test(basename(cwd))
|
|
192
|
+
? basename(dirname(cwd))
|
|
193
|
+
: basename(cwd);
|
|
130
194
|
const route = this.routes.get(`${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`);
|
|
131
195
|
const entry = route?.selected?.entry;
|
|
132
|
-
if (!entry ||
|
|
196
|
+
if (!entry ||
|
|
197
|
+
!this.valid(entry) ||
|
|
198
|
+
entry.sessionId !== sessionId ||
|
|
199
|
+
entry.clientRequestId !== clientRequestId)
|
|
133
200
|
return;
|
|
134
201
|
entry.codexDispatchedAt = new Date().toISOString();
|
|
135
202
|
entry.codexDispatchMark = performance.now();
|
|
@@ -137,7 +204,9 @@ export class WorkspaceModelBroker {
|
|
|
137
204
|
async credential(entry, refresh = false) {
|
|
138
205
|
if (!this.valid(entry))
|
|
139
206
|
throw new Error('model_access_denied');
|
|
140
|
-
if (!refresh &&
|
|
207
|
+
if (!refresh &&
|
|
208
|
+
entry.credential &&
|
|
209
|
+
Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
|
|
141
210
|
return entry.credential;
|
|
142
211
|
let pending = this.pending.get(entry);
|
|
143
212
|
if (!pending) {
|
|
@@ -150,26 +219,48 @@ export class WorkspaceModelBroker {
|
|
|
150
219
|
const config = this.state.config;
|
|
151
220
|
const endpoint = platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.modelCredentials);
|
|
152
221
|
const response = await fetch(endpoint, {
|
|
153
|
-
method: 'POST',
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
222
|
+
method: 'POST',
|
|
223
|
+
redirect: 'error',
|
|
224
|
+
signal: AbortSignal.timeout(10_000),
|
|
225
|
+
headers: {
|
|
226
|
+
authorization: `Bearer ${config.controlToken}`,
|
|
227
|
+
'content-type': 'application/json',
|
|
228
|
+
'playcraft-env': config.environment,
|
|
229
|
+
...(this.capture.supported ? { 'x-playcraft-model-capture-protocol': '1' } : {}),
|
|
230
|
+
},
|
|
231
|
+
body: JSON.stringify({
|
|
232
|
+
runtimeId: config.runtimeId,
|
|
233
|
+
generation: config.generation,
|
|
234
|
+
runtimeEpoch: entry.epoch,
|
|
235
|
+
workspaceId: entry.workspaceId,
|
|
157
236
|
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
|
|
158
237
|
...(entry.clientRequestId ? { clientRequestId: entry.clientRequestId } : {}),
|
|
159
238
|
...(entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {}),
|
|
160
|
-
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {})
|
|
239
|
+
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}),
|
|
240
|
+
}),
|
|
161
241
|
});
|
|
162
242
|
if (!response.ok) {
|
|
163
|
-
const error = await response.json().catch(() => ({}));
|
|
164
|
-
throw new Error([
|
|
243
|
+
const error = (await response.json().catch(() => ({})));
|
|
244
|
+
throw new Error([
|
|
245
|
+
'model_config_required',
|
|
246
|
+
'model_config_denied',
|
|
247
|
+
'reasoning_effort_denied',
|
|
248
|
+
'reasoning_effort_unsupported',
|
|
249
|
+
].includes(error.reason ?? '')
|
|
250
|
+
? error.reason
|
|
251
|
+
: 'model_credential_unavailable');
|
|
165
252
|
}
|
|
166
253
|
const credential = credentialSchema.parse(await response.json());
|
|
167
254
|
const claims = credential.claims;
|
|
168
|
-
if (!this.valid(entry) ||
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
255
|
+
if (!this.valid(entry) ||
|
|
256
|
+
claims.workspaceId !== entry.workspaceId ||
|
|
257
|
+
claims.runtimeId !== config.runtimeId ||
|
|
258
|
+
claims.userId !== config.userId ||
|
|
259
|
+
claims.environment !== config.environment ||
|
|
260
|
+
claims.generation !== config.generation ||
|
|
261
|
+
claims.runtimeEpoch !== entry.epoch ||
|
|
262
|
+
(entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId) ||
|
|
263
|
+
Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
173
264
|
throw new Error('model_credential_scope_mismatch');
|
|
174
265
|
entry.modelConfigId = credential.configId;
|
|
175
266
|
entry.credential = credential;
|
|
@@ -182,6 +273,9 @@ export class WorkspaceModelBroker {
|
|
|
182
273
|
const abort = new AbortController();
|
|
183
274
|
this.requests.add(abort);
|
|
184
275
|
let activeRoute;
|
|
276
|
+
let capturedRequest;
|
|
277
|
+
let capturedResponse;
|
|
278
|
+
let requestEnded = false;
|
|
185
279
|
const cancel = () => abort.abort();
|
|
186
280
|
response.once('close', cancel);
|
|
187
281
|
try {
|
|
@@ -194,10 +288,21 @@ export class WorkspaceModelBroker {
|
|
|
194
288
|
const contextRequest = match?.[2] === 'execution-context';
|
|
195
289
|
const referencesRequest = match?.[2] === 'remix-references';
|
|
196
290
|
const skillsRequest = match?.[2]?.startsWith('skill-discovery/');
|
|
197
|
-
const expected = Buffer.from(route
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
291
|
+
const expected = Buffer.from(route
|
|
292
|
+
? `Bearer ${generationRequest || contextRequest
|
|
293
|
+
? route.generationCapability
|
|
294
|
+
: referencesRequest
|
|
295
|
+
? route.referencesCapability
|
|
296
|
+
: skillsRequest
|
|
297
|
+
? route.skillsCapability
|
|
298
|
+
: route.capability}`
|
|
299
|
+
: '');
|
|
300
|
+
if (request.method !== 'POST' ||
|
|
301
|
+
!entry ||
|
|
302
|
+
!expected.length ||
|
|
303
|
+
presented.length !== expected.length ||
|
|
304
|
+
!timingSafeEqual(presented, expected) ||
|
|
305
|
+
!this.valid(entry)) {
|
|
201
306
|
response.writeHead(403);
|
|
202
307
|
response.end();
|
|
203
308
|
return;
|
|
@@ -240,12 +345,23 @@ export class WorkspaceModelBroker {
|
|
|
240
345
|
return;
|
|
241
346
|
}
|
|
242
347
|
const rejectOversizedBody = () => {
|
|
243
|
-
response.writeHead(413, {
|
|
244
|
-
|
|
245
|
-
|
|
348
|
+
response.writeHead(413, {
|
|
349
|
+
'content-type': 'application/json',
|
|
350
|
+
'cache-control': 'no-store',
|
|
351
|
+
});
|
|
352
|
+
response.end(JSON.stringify({
|
|
353
|
+
error: {
|
|
354
|
+
message: 'model_request_too_large',
|
|
355
|
+
type: 'invalid_request_error',
|
|
356
|
+
code: 'model_request_too_large',
|
|
357
|
+
maxBytes: MAX_MODEL_REQUEST_BYTES,
|
|
358
|
+
},
|
|
359
|
+
}));
|
|
246
360
|
};
|
|
247
361
|
const declaredLength = request.headers['content-length'];
|
|
248
|
-
if (declaredLength !== undefined &&
|
|
362
|
+
if (declaredLength !== undefined &&
|
|
363
|
+
/^\d+$/.test(declaredLength) &&
|
|
364
|
+
Number(declaredLength) > MAX_MODEL_REQUEST_BYTES) {
|
|
249
365
|
rejectOversizedBody();
|
|
250
366
|
return;
|
|
251
367
|
}
|
|
@@ -256,10 +372,21 @@ export class WorkspaceModelBroker {
|
|
|
256
372
|
const credentialMark = performance.now();
|
|
257
373
|
const credential = await this.credential(entry);
|
|
258
374
|
const credentialWaitMs = elapsed(credentialMark);
|
|
375
|
+
const modelCallId = randomUUID();
|
|
376
|
+
const candidateCaptureOrigin = credential.claims.traceOrigin;
|
|
377
|
+
const captured = candidateCaptureOrigin?.sessionId === entry.sessionId &&
|
|
378
|
+
candidateCaptureOrigin?.clientRequestId === entry.clientRequestId
|
|
379
|
+
? this.capture.begin(modelCallId, credential)
|
|
380
|
+
: undefined;
|
|
381
|
+
capturedRequest = captured?.artifact('codex_request', request.headers['content-type'] ?? 'application/json');
|
|
382
|
+
capturedResponse = captured?.artifact('codex_response');
|
|
383
|
+
if (capturedResponse)
|
|
384
|
+
captureSubmittedResponse(response, capturedResponse);
|
|
259
385
|
const requestReadMark = performance.now();
|
|
260
386
|
const chunks = [];
|
|
261
387
|
let size = 0;
|
|
262
388
|
for await (const chunk of request) {
|
|
389
|
+
capturedRequest?.append(chunk);
|
|
263
390
|
size += chunk.length;
|
|
264
391
|
if (size > MAX_MODEL_REQUEST_BYTES) {
|
|
265
392
|
rejectOversizedBody();
|
|
@@ -268,6 +395,8 @@ export class WorkspaceModelBroker {
|
|
|
268
395
|
chunks.push(Buffer.from(chunk));
|
|
269
396
|
}
|
|
270
397
|
const requestReadMs = elapsed(requestReadMark);
|
|
398
|
+
capturedRequest?.finish({ sourceEnd: 'eof', protocolTerminal: 'not_applicable' });
|
|
399
|
+
requestEnded = true;
|
|
271
400
|
if (!this.valid(entry))
|
|
272
401
|
throw new Error('model_access_denied');
|
|
273
402
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
@@ -288,9 +417,12 @@ export class WorkspaceModelBroker {
|
|
|
288
417
|
const reasoning = body.reasoning;
|
|
289
418
|
const modelName = selected.model.split('/').pop() ?? selected.model;
|
|
290
419
|
const effort = entry.reasoningEffort === 'max' && /^o\d|gpt-[5-9]/i.test(modelName)
|
|
291
|
-
? 'xhigh'
|
|
420
|
+
? 'xhigh'
|
|
421
|
+
: entry.reasoningEffort;
|
|
292
422
|
body.reasoning = {
|
|
293
|
-
...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning)
|
|
423
|
+
...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning)
|
|
424
|
+
? reasoning
|
|
425
|
+
: {}),
|
|
294
426
|
effort,
|
|
295
427
|
};
|
|
296
428
|
}
|
|
@@ -299,26 +431,58 @@ export class WorkspaceModelBroker {
|
|
|
299
431
|
const dispatchedMark = performance.now();
|
|
300
432
|
const beforeDispatchMs = elapsed(receivedMark);
|
|
301
433
|
const candidateOrigin = credential.claims.traceOrigin;
|
|
302
|
-
const origin = candidateOrigin?.sessionId === entry.sessionId &&
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
434
|
+
const origin = candidateOrigin?.sessionId === entry.sessionId &&
|
|
435
|
+
candidateOrigin?.clientRequestId === entry.clientRequestId
|
|
436
|
+
? candidateOrigin
|
|
437
|
+
: undefined;
|
|
438
|
+
const broker = {
|
|
439
|
+
receivedAt,
|
|
440
|
+
dispatchedAt,
|
|
441
|
+
credentialWaitMs,
|
|
442
|
+
requestReadMs,
|
|
443
|
+
beforeDispatchMs,
|
|
444
|
+
};
|
|
306
445
|
let attemptId;
|
|
307
446
|
let outcome = 'error';
|
|
308
447
|
try {
|
|
309
448
|
const upstream = await fetch(platformEndpoint(this.state.config.publicBaseUrl, modelResponsesPath(credential.configId)), {
|
|
310
|
-
method: 'POST',
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
449
|
+
method: 'POST',
|
|
450
|
+
redirect: 'error',
|
|
451
|
+
signal: abort.signal,
|
|
452
|
+
headers: {
|
|
453
|
+
authorization: `Bearer ${credential.token}`,
|
|
454
|
+
'content-type': 'application/json',
|
|
455
|
+
'x-playcraft-model-call-id': modelCallId,
|
|
456
|
+
...(origin
|
|
457
|
+
? {
|
|
458
|
+
'x-playcraft-model-journey': JSON.stringify({
|
|
459
|
+
version: 1,
|
|
460
|
+
runtimeOperationId: origin.runtimeOperationId,
|
|
461
|
+
sessionId: origin.sessionId,
|
|
462
|
+
clientRequestId: origin.clientRequestId,
|
|
463
|
+
...(entry.codexDispatchedAt
|
|
464
|
+
? { codexDispatchedAt: entry.codexDispatchedAt }
|
|
465
|
+
: {}),
|
|
466
|
+
brokerReceivedAt: receivedAt,
|
|
467
|
+
brokerDispatchedAt: dispatchedAt,
|
|
468
|
+
credentialWaitMs,
|
|
469
|
+
requestReadMs,
|
|
470
|
+
beforeDispatchMs,
|
|
471
|
+
...(entry.codexDispatchMark === undefined
|
|
472
|
+
? {}
|
|
473
|
+
: {
|
|
474
|
+
codexDispatchToBrokerMs: Math.max(0, Math.round(receivedMark - entry.codexDispatchMark)),
|
|
475
|
+
}),
|
|
476
|
+
}),
|
|
477
|
+
}
|
|
478
|
+
: {}),
|
|
479
|
+
...(selected?.feedback
|
|
480
|
+
? {
|
|
481
|
+
'x-playcraft-cloud-session-id': selected.feedback.sessionId,
|
|
482
|
+
'x-playcraft-client-request-id': selected.feedback.clientRequestId,
|
|
483
|
+
}
|
|
484
|
+
: {}),
|
|
485
|
+
},
|
|
322
486
|
body: JSON.stringify(body),
|
|
323
487
|
});
|
|
324
488
|
broker.responseHeadersAt = new Date().toISOString();
|
|
@@ -343,6 +507,7 @@ export class WorkspaceModelBroker {
|
|
|
343
507
|
if (upstream.body)
|
|
344
508
|
await pipeline(Readable.fromWeb(upstream.body), new Transform({
|
|
345
509
|
transform(chunk, _encoding, callback) {
|
|
510
|
+
capturedResponse?.upstreamObserved(chunk.length);
|
|
346
511
|
if (chunk.length && broker.firstBodyByteAt === undefined) {
|
|
347
512
|
broker.firstBodyByteAt = new Date().toISOString();
|
|
348
513
|
broker.proxyFirstBodyByteMs = elapsed(dispatchedMark);
|
|
@@ -364,21 +529,37 @@ export class WorkspaceModelBroker {
|
|
|
364
529
|
broker.proxyResponseDurationMs = elapsed(dispatchedMark);
|
|
365
530
|
if (origin && attemptId) {
|
|
366
531
|
// Telemetry is sent after transfer and cannot delay or change the Codex model response.
|
|
367
|
-
void this.reportObservation(credential.token, {
|
|
368
|
-
|
|
369
|
-
|
|
532
|
+
void this.reportObservation(credential.token, {
|
|
533
|
+
version: 1,
|
|
534
|
+
attemptId,
|
|
535
|
+
runtimeOperationId: origin.runtimeOperationId,
|
|
536
|
+
sessionId: origin.sessionId,
|
|
537
|
+
clientRequestId: origin.clientRequestId,
|
|
538
|
+
broker,
|
|
539
|
+
outcome,
|
|
540
|
+
});
|
|
370
541
|
}
|
|
371
542
|
}
|
|
372
543
|
}
|
|
373
544
|
catch {
|
|
374
545
|
if (!response.headersSent) {
|
|
375
|
-
response.writeHead(503, {
|
|
376
|
-
|
|
546
|
+
response.writeHead(503, {
|
|
547
|
+
'content-type': 'application/json',
|
|
548
|
+
'cache-control': 'no-store',
|
|
549
|
+
});
|
|
550
|
+
response.end(JSON.stringify({
|
|
551
|
+
error: { message: 'model_credential_unavailable', type: 'server_error' },
|
|
552
|
+
}));
|
|
377
553
|
}
|
|
378
554
|
else
|
|
379
555
|
response.destroy();
|
|
380
556
|
}
|
|
381
557
|
finally {
|
|
558
|
+
if (!requestEnded)
|
|
559
|
+
capturedRequest?.finish({
|
|
560
|
+
sourceEnd: abort.signal.aborted ? 'aborted' : 'io_error',
|
|
561
|
+
protocolTerminal: 'unknown',
|
|
562
|
+
});
|
|
382
563
|
response.off('close', cancel);
|
|
383
564
|
this.requests.delete(abort);
|
|
384
565
|
if (activeRoute) {
|
|
@@ -392,12 +573,17 @@ export class WorkspaceModelBroker {
|
|
|
392
573
|
async reportObservation(token, input) {
|
|
393
574
|
try {
|
|
394
575
|
const response = await fetch(platformEndpoint(this.state.config.publicBaseUrl, '/api/llm-proxy/runtime-observations'), {
|
|
395
|
-
method: 'POST',
|
|
396
|
-
|
|
576
|
+
method: 'POST',
|
|
577
|
+
redirect: 'error',
|
|
578
|
+
signal: AbortSignal.timeout(3_000),
|
|
579
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
580
|
+
body: JSON.stringify(input),
|
|
397
581
|
});
|
|
398
582
|
await response.body?.cancel();
|
|
399
583
|
}
|
|
400
|
-
catch {
|
|
584
|
+
catch {
|
|
585
|
+
/* Missing observations remain unknown; never replay a model request for telemetry. */
|
|
586
|
+
}
|
|
401
587
|
}
|
|
402
588
|
cancelSession(cwd, sessionId) {
|
|
403
589
|
const routeKey = `${/^(basic|remix-)/.test(basename(cwd)) ? basename(dirname(cwd)) : basename(cwd)}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`;
|
|
@@ -424,7 +610,8 @@ export class WorkspaceModelBroker {
|
|
|
424
610
|
request.abort();
|
|
425
611
|
if (server) {
|
|
426
612
|
server.closeAllConnections();
|
|
427
|
-
await new Promise(resolve => server.close(() => resolve()));
|
|
613
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
428
614
|
}
|
|
615
|
+
await this.capture.close();
|
|
429
616
|
}
|
|
430
617
|
}
|