reploid 0.0.0-stage → 0.1.0
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/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +534 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +83 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/** Bounded framing for a dedicated, reliable, ordered Pack-custody data channel.
|
|
2
|
+
* Signatures and authorized chunk commitments remain owned by peer-pack-custody.
|
|
3
|
+
* Counts describe application frames, not SCTP/IP bytes or relay charges.
|
|
4
|
+
*/
|
|
5
|
+
const SCHEMA = 'reploid.pool.pack-custody-channel/v1';
|
|
6
|
+
const HEADER_BYTES = 24;
|
|
7
|
+
const ID = /^[0-9a-f]{32}$/;
|
|
8
|
+
const positive = (value) => Number.isSafeInteger(value) && value > 0;
|
|
9
|
+
const assert = (condition, message) => { if (!condition) throw new Error(`Pack channel: ${message}`); };
|
|
10
|
+
|
|
11
|
+
export function createPeerPackDataChannel({ channel, limits, serve = null }) {
|
|
12
|
+
assert(channel?.ordered === true && channel.maxRetransmits == null && channel.maxPacketLifeTime == null,
|
|
13
|
+
'a reliable ordered dedicated data channel is required');
|
|
14
|
+
assert(typeof channel.send === 'function' && typeof channel.addEventListener === 'function', 'data channel port required');
|
|
15
|
+
for (const name of ['maxFrameBytes', 'maxControlBytes', 'maxChunkBytes', 'maxBufferedBytes', 'maxPendingRequests', 'maxTransferBytes', 'timeoutMs']) {
|
|
16
|
+
assert(positive(limits?.[name]), `explicit ${name} required`);
|
|
17
|
+
}
|
|
18
|
+
assert(limits.maxFrameBytes > HEADER_BYTES && limits.maxFrameBytes <= limits.maxBufferedBytes
|
|
19
|
+
&& limits.maxControlBytes <= limits.maxBufferedBytes && limits.maxChunkBytes <= 0xffffffff
|
|
20
|
+
&& limits.timeoutMs <= 2147483647, 'invalid framing limits');
|
|
21
|
+
const policy = structuredClone(limits);
|
|
22
|
+
const fragmentBytes = policy.maxFrameBytes - HEADER_BYTES;
|
|
23
|
+
channel.binaryType = 'arraybuffer';
|
|
24
|
+
channel.bufferedAmountLowThreshold = 0;
|
|
25
|
+
const pending = new Map();
|
|
26
|
+
const inbound = new Map();
|
|
27
|
+
const lifecycle = new AbortController();
|
|
28
|
+
const counters = { sentFrameBytes: 0, receivedFrameBytes: 0, sentFrames: 0, receivedFrames: 0,
|
|
29
|
+
completedRequests: 0, cancelledRequests: 0, interruptedPayloadBytes: 0, discardedFrames: 0 };
|
|
30
|
+
let closed = false;
|
|
31
|
+
|
|
32
|
+
function finish(id, error, response) {
|
|
33
|
+
const entry = pending.get(id);
|
|
34
|
+
if (!entry) return;
|
|
35
|
+
pending.delete(id);
|
|
36
|
+
clearTimeout(entry.timer);
|
|
37
|
+
entry.signal?.removeEventListener('abort', entry.abort);
|
|
38
|
+
if (error) {
|
|
39
|
+
counters.interruptedPayloadBytes += entry.receivedBytes;
|
|
40
|
+
entry.reject(error);
|
|
41
|
+
} else {
|
|
42
|
+
counters.completedRequests += 1;
|
|
43
|
+
entry.resolve(response);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function close(reason = 'channel closed') {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
lifecycle.abort();
|
|
51
|
+
for (const controller of inbound.values()) controller.abort();
|
|
52
|
+
inbound.clear();
|
|
53
|
+
for (const id of pending.keys()) finish(id, new Error(`Pack channel: ${reason}`));
|
|
54
|
+
channel.removeEventListener('message', onMessage);
|
|
55
|
+
channel.removeEventListener('close', onClose);
|
|
56
|
+
channel.removeEventListener('error', onClose);
|
|
57
|
+
channel.close();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function send(data, signal = lifecycle.signal) {
|
|
61
|
+
const byteLength = typeof data === 'string' ? new TextEncoder().encode(data).byteLength : data.byteLength;
|
|
62
|
+
assert(!closed && channel.readyState === 'open' && !signal.aborted, 'channel unavailable');
|
|
63
|
+
assert(counters.sentFrameBytes + byteLength <= policy.maxTransferBytes, 'outgoing byte budget exhausted');
|
|
64
|
+
if (channel.bufferedAmount + byteLength > policy.maxBufferedBytes) {
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const finish = (error) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
channel.removeEventListener('bufferedamountlow', low);
|
|
69
|
+
signal.removeEventListener('abort', abort);
|
|
70
|
+
lifecycle.signal.removeEventListener('abort', abort);
|
|
71
|
+
error ? reject(error) : resolve();
|
|
72
|
+
};
|
|
73
|
+
const low = () => finish();
|
|
74
|
+
const abort = () => finish(new Error('Pack channel: send cancelled'));
|
|
75
|
+
const timer = setTimeout(() => finish(new Error('Pack channel: backpressure timeout')), policy.timeoutMs);
|
|
76
|
+
channel.addEventListener('bufferedamountlow', low, { once: true });
|
|
77
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
78
|
+
lifecycle.signal.addEventListener('abort', abort, { once: true });
|
|
79
|
+
if (closed || signal.aborted) abort();
|
|
80
|
+
else if (channel.bufferedAmount + byteLength <= policy.maxBufferedBytes) low();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
assert(!closed && !signal.aborted && channel.readyState === 'open', 'channel unavailable');
|
|
84
|
+
assert(channel.bufferedAmount + byteLength <= policy.maxBufferedBytes, 'send buffer limit exceeded');
|
|
85
|
+
assert(counters.sentFrameBytes + byteLength <= policy.maxTransferBytes, 'outgoing byte budget exhausted');
|
|
86
|
+
channel.send(data);
|
|
87
|
+
counters.sentFrameBytes += byteLength;
|
|
88
|
+
counters.sentFrames += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function control(value, signal) {
|
|
92
|
+
const text = JSON.stringify({ schema: SCHEMA, ...value });
|
|
93
|
+
assert(new TextEncoder().encode(text).byteLength <= policy.maxControlBytes, 'control frame too large');
|
|
94
|
+
return send(text, signal);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function supply(frame) {
|
|
98
|
+
assert(typeof serve === 'function', 'serving is not enabled');
|
|
99
|
+
assert(!inbound.has(frame.id) && inbound.size < policy.maxPendingRequests, 'incoming request limit or duplicate');
|
|
100
|
+
assert(positive(frame.request?.sizeBytes) && frame.request.sizeBytes <= policy.maxChunkBytes, 'incoming chunk limit');
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
inbound.set(frame.id, controller);
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), policy.timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
const response = await serve(structuredClone(frame.request), { signal: controller.signal });
|
|
106
|
+
if (controller.signal.aborted || closed) return;
|
|
107
|
+
assert(response?.bytes instanceof Uint8Array && response.bytes.length === frame.request.sizeBytes, 'supplier response geometry');
|
|
108
|
+
await control({ type: 'response', id: frame.id, message: response.message, byteLength: response.bytes.length }, controller.signal);
|
|
109
|
+
for (let offset = 0; offset < response.bytes.length; offset += fragmentBytes) {
|
|
110
|
+
const part = response.bytes.subarray(offset, offset + fragmentBytes);
|
|
111
|
+
const bytes = new Uint8Array(HEADER_BYTES + part.length);
|
|
112
|
+
for (let index = 0; index < 16; index += 1) bytes[index] = parseInt(frame.id.slice(index * 2, index * 2 + 2), 16);
|
|
113
|
+
const view = new DataView(bytes.buffer);
|
|
114
|
+
view.setUint32(16, offset);
|
|
115
|
+
view.setUint32(20, part.length);
|
|
116
|
+
bytes.set(part, HEADER_BYTES);
|
|
117
|
+
await send(bytes.buffer, controller.signal);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!closed && !controller.signal.aborted) {
|
|
121
|
+
await control({ type: 'error', id: frame.id, error: String(error.message).slice(0, 256) }, controller.signal).catch(() => close('error delivery failed'));
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
inbound.delete(frame.id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function receiveControl(text) {
|
|
130
|
+
const frame = JSON.parse(text);
|
|
131
|
+
assert(frame?.schema === SCHEMA && ID.test(frame.id), 'invalid control frame');
|
|
132
|
+
if (frame.type === 'request') {
|
|
133
|
+
supply(frame).catch((error) => close(error.message));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (frame.type === 'cancel') {
|
|
137
|
+
inbound.get(frame.id)?.abort();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const entry = pending.get(frame.id);
|
|
141
|
+
if (!entry) { counters.discardedFrames += 1; return; }
|
|
142
|
+
if (frame.type === 'error') {
|
|
143
|
+
finish(frame.id, new Error(`Pack channel supplier: ${String(frame.error).slice(0, 256)}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
assert(frame.type === 'response' && !entry.bytes && frame.byteLength === entry.maxBytes, 'response binding or geometry');
|
|
147
|
+
assert(frame.message && typeof frame.message === 'object', 'signed response required');
|
|
148
|
+
entry.message = frame.message;
|
|
149
|
+
entry.bytes = new Uint8Array(frame.byteLength);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function receiveBinary(buffer) {
|
|
153
|
+
assert(buffer.byteLength > HEADER_BYTES, 'empty binary frame');
|
|
154
|
+
const bytes = new Uint8Array(buffer);
|
|
155
|
+
const id = Array.from(bytes.subarray(0, 16), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
156
|
+
const entry = pending.get(id);
|
|
157
|
+
if (!entry) { counters.discardedFrames += 1; return; }
|
|
158
|
+
assert(entry.bytes, 'binary frame before response');
|
|
159
|
+
const view = new DataView(buffer);
|
|
160
|
+
const offset = view.getUint32(16);
|
|
161
|
+
const length = view.getUint32(20);
|
|
162
|
+
assert(offset % fragmentBytes === 0 && offset < entry.maxBytes
|
|
163
|
+
&& length === Math.min(fragmentBytes, entry.maxBytes - offset) && length === bytes.length - HEADER_BYTES, 'binary frame range');
|
|
164
|
+
if (entry.offsets.has(offset)) {
|
|
165
|
+
assert(bytes.subarray(HEADER_BYTES).every((byte, index) => entry.bytes[offset + index] === byte), 'conflicting duplicate fragment');
|
|
166
|
+
counters.discardedFrames += 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
entry.bytes.set(bytes.subarray(HEADER_BYTES), offset);
|
|
170
|
+
entry.offsets.add(offset);
|
|
171
|
+
entry.receivedBytes += length;
|
|
172
|
+
if (entry.receivedBytes === entry.maxBytes) finish(id, null, { message: entry.message, bytes: entry.bytes });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onMessage(event) {
|
|
176
|
+
try {
|
|
177
|
+
const data = event.data;
|
|
178
|
+
const text = typeof data === 'string';
|
|
179
|
+
assert(text || data instanceof ArrayBuffer, 'unsupported frame type');
|
|
180
|
+
const size = text ? new TextEncoder().encode(data).byteLength : data.byteLength;
|
|
181
|
+
counters.receivedFrameBytes += size;
|
|
182
|
+
counters.receivedFrames += 1;
|
|
183
|
+
assert(size <= (text ? policy.maxControlBytes : policy.maxFrameBytes), 'frame limit exceeded');
|
|
184
|
+
assert(counters.receivedFrameBytes <= policy.maxTransferBytes, 'incoming byte budget exhausted');
|
|
185
|
+
text ? receiveControl(data) : receiveBinary(data);
|
|
186
|
+
} catch (error) { close(error.message); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function onClose() { close('transport disconnected'); }
|
|
190
|
+
channel.addEventListener('message', onMessage);
|
|
191
|
+
channel.addEventListener('close', onClose);
|
|
192
|
+
channel.addEventListener('error', onClose);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
requestChunk(request, { signal, maxBytes }) {
|
|
196
|
+
assert(!closed && channel.readyState === 'open', 'channel unavailable');
|
|
197
|
+
assert(positive(maxBytes) && maxBytes === request?.sizeBytes && maxBytes <= policy.maxChunkBytes, 'requested chunk limit');
|
|
198
|
+
assert(pending.size < policy.maxPendingRequests && !signal?.aborted, 'request cancelled or pending limit exceeded');
|
|
199
|
+
const id = crypto.randomUUID().replaceAll('-', '');
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
const abort = () => {
|
|
202
|
+
counters.cancelledRequests += 1;
|
|
203
|
+
finish(id, new Error('Pack channel: request cancelled or timed out'));
|
|
204
|
+
control({ type: 'cancel', id }).catch(() => close('cancellation delivery failed'));
|
|
205
|
+
};
|
|
206
|
+
const entry = { resolve, reject, signal, abort, maxBytes, receivedBytes: 0, bytes: null, offsets: new Set(),
|
|
207
|
+
timer: setTimeout(abort, policy.timeoutMs) };
|
|
208
|
+
pending.set(id, entry);
|
|
209
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
210
|
+
control({ type: 'request', id, request }).catch((error) => finish(id, error));
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
close,
|
|
214
|
+
getReceipt() { return { schema: SCHEMA, ...counters, pendingRequests: pending.size, inboundRequests: inbound.size,
|
|
215
|
+
closed, wireBytes: null, relayBytes: null }; },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Resolve selections through the authorized catalog, never caller-supplied URLs. */
|
|
2
|
+
export function selectWorkAdapters(model, selections = model.adapters || []) {
|
|
3
|
+
if (!Array.isArray(selections) || selections.length > 1) throw new Error('Select at most one adapter');
|
|
4
|
+
return selections.map(selection => {
|
|
5
|
+
const entry = model.availableAdapters?.find(adapter => adapter.identity === selection.identity
|
|
6
|
+
&& adapter.baseModelIdentity === model.identity);
|
|
7
|
+
if (!entry) throw new Error('Adapter is not authorized for this exact base artifact');
|
|
8
|
+
return structuredClone(entry);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Acquire catalog-pinned adapter bytes; Doppler owns compatibility and tensor identity. */
|
|
13
|
+
export function createWorkAdapterResolver({ models, acquire, fetchImpl = globalThis.fetch }) {
|
|
14
|
+
const catalog = structuredClone(models).flatMap(model => model.availableAdapters || []);
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
return async (selection, { signal }) => {
|
|
17
|
+
signal.throwIfAborted();
|
|
18
|
+
const entry = catalog.find(item => item.identity === selection.identity
|
|
19
|
+
&& item.baseModelIdentity === selection.baseModelIdentity);
|
|
20
|
+
if (!entry) throw new Error('Selected adapter is not in the authorized catalog');
|
|
21
|
+
let bytes = cache.get(entry.identity);
|
|
22
|
+
if (!bytes) {
|
|
23
|
+
if (acquire) bytes = new Uint8Array(await acquire(entry.artifact, { signal })).slice();
|
|
24
|
+
else {
|
|
25
|
+
bytes = await fetchAdapterFile(entry.artifact, { signal, fetchImpl });
|
|
26
|
+
}
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
const digest = [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))]
|
|
29
|
+
.map(value => value.toString(16).padStart(2, '0')).join('');
|
|
30
|
+
if (bytes.byteLength !== entry.artifact.sizeBytes || 'sha256:' + digest !== entry.artifact.hash) {
|
|
31
|
+
throw new Error('Adapter file identity mismatch');
|
|
32
|
+
}
|
|
33
|
+
signal.throwIfAborted(); cache.set(entry.identity, bytes.slice());
|
|
34
|
+
}
|
|
35
|
+
return { manifest: structuredClone(entry.manifest), options: {
|
|
36
|
+
weightsLayout: entry.weightsLayout, readOPFS: undefined, writeOPFS: undefined,
|
|
37
|
+
fetchUrl: async url => {
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
if (url !== entry.manifest.weightsPath) throw new Error('Adapter requested an undeclared file');
|
|
40
|
+
return bytes.slice().buffer;
|
|
41
|
+
}
|
|
42
|
+
} };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Bounded origin delivery; the resolver or custody owner verifies the resulting digest. */
|
|
47
|
+
export async function fetchAdapterFile(artifact, { signal, fetchImpl = globalThis.fetch }) {
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
if (!Number.isSafeInteger(artifact.sizeBytes) || artifact.sizeBytes <= 0) throw new Error('Invalid adapter file size');
|
|
50
|
+
const response = await fetchImpl(artifact.url, { signal });
|
|
51
|
+
if (!response.ok) throw new Error(`Adapter acquisition HTTP ${response.status}`);
|
|
52
|
+
const chunks = []; let size = 0;
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
try {
|
|
55
|
+
while (size <= artifact.sizeBytes) {
|
|
56
|
+
const { value, done } = await reader.read();
|
|
57
|
+
if (done) break;
|
|
58
|
+
if (!value?.byteLength) throw new Error('Adapter stream made no progress');
|
|
59
|
+
signal.throwIfAborted(); size += value.byteLength;
|
|
60
|
+
if (size > artifact.sizeBytes) throw new Error('Adapter exceeds its declared file size');
|
|
61
|
+
chunks.push(value);
|
|
62
|
+
}
|
|
63
|
+
} finally { await reader.cancel(); reader.releaseLock(); }
|
|
64
|
+
const bytes = new Uint8Array(size); let offset = 0;
|
|
65
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
66
|
+
return bytes;
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared device admission. Running work owns its slot until cleanup settles. */
|
|
2
|
+
// One borrowed GPU operation per runtime service, shared by threads and suppliers.
|
|
3
|
+
// Cancellation removes a waiter; a running operation keeps its slot until settlement.
|
|
4
|
+
const queues = new WeakMap();
|
|
5
|
+
export async function withWorkDevice(service, signal, operation, onProgress = () => {}) {
|
|
6
|
+
signal.throwIfAborted();
|
|
7
|
+
let queue = queues.get(service);
|
|
8
|
+
if (!queue) { queue = { busy: false, waiting: [] }; queues.set(service, queue); }
|
|
9
|
+
await new Promise((resolve, reject) => {
|
|
10
|
+
const waiter = { enter() {
|
|
11
|
+
signal.removeEventListener('abort', abort);
|
|
12
|
+
queue.busy = true; resolve();
|
|
13
|
+
} };
|
|
14
|
+
const remove = error => {
|
|
15
|
+
queue.waiting = queue.waiting.filter(item => item !== waiter);
|
|
16
|
+
signal.removeEventListener('abort', abort);
|
|
17
|
+
reject(error);
|
|
18
|
+
};
|
|
19
|
+
const abort = () => remove(signal.reason);
|
|
20
|
+
if (!queue.busy) waiter.enter();
|
|
21
|
+
else {
|
|
22
|
+
queue.waiting.push(waiter);
|
|
23
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
24
|
+
try { onProgress('Waiting for this device'); }
|
|
25
|
+
catch (error) { remove(error); }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
try { signal.throwIfAborted(); return await operation(); }
|
|
29
|
+
finally {
|
|
30
|
+
queue.busy = false;
|
|
31
|
+
queue.waiting.shift()?.enter();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { GENERATION_CONTRACT } from '../../../shared/config/doppler-generation-contract.js';
|
|
2
|
+
|
|
3
|
+
/** Consume Doppler's public scoped stream: ordered text, then exactly one completion.
|
|
4
|
+
* A transport ending is not completion. Cancellation awaits iterator cleanup.
|
|
5
|
+
*/
|
|
6
|
+
export async function consumeWorkGenerationStream(events, { signal, onDelta, maxOutcomeCharacters }) {
|
|
7
|
+
let text = '', terminal = null;
|
|
8
|
+
for await (const event of events) {
|
|
9
|
+
signal.throwIfAborted();
|
|
10
|
+
if (terminal) throw new Error('Doppler stream emitted an event after completion');
|
|
11
|
+
if (event?.schema !== 'doppler.generation-event/v1'
|
|
12
|
+
|| !['text-delta', 'complete'].includes(event.type)) throw new Error('Doppler stream emitted an unsupported event');
|
|
13
|
+
if (event.type === 'text-delta') {
|
|
14
|
+
if (typeof event.text !== 'string') throw new Error('Doppler stream emitted an invalid text delta');
|
|
15
|
+
text += event.text;
|
|
16
|
+
if (text.length > maxOutcomeCharacters) throw new Error('Model response exceeded the configured size limit');
|
|
17
|
+
await onDelta(event.text);
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
} else {
|
|
20
|
+
if (event.outputText !== text) throw new Error('Doppler completed text differs from its stream');
|
|
21
|
+
terminal = { content: text, completion: readWorkGenerationCompletion(event) };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
if (!terminal) throw new Error('Doppler stream ended without completion');
|
|
26
|
+
return terminal;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Preserve Doppler's declared termination; missing legacy metadata stays unknown. */
|
|
30
|
+
export function readWorkGenerationCompletion(result) {
|
|
31
|
+
const value = Object.hasOwn(result, 'completion') ? result.completion : result.evidence?.output?.completion;
|
|
32
|
+
if (value == null) return null;
|
|
33
|
+
if (value.schema !== GENERATION_CONTRACT.completionSchema
|
|
34
|
+
|| !GENERATION_CONTRACT.stoppingReasons.includes(value.stopReason)
|
|
35
|
+
|| !Number.isSafeInteger(value.promptTokenCount) || value.promptTokenCount < 0
|
|
36
|
+
|| !Number.isSafeInteger(value.generatedTokenCount) || value.generatedTokenCount < 0) {
|
|
37
|
+
throw new Error('Doppler supplied invalid generation completion metadata');
|
|
38
|
+
}
|
|
39
|
+
return { schema: value.schema, stopReason: value.stopReason,
|
|
40
|
+
promptTokenCount: value.promptTokenCount, generatedTokenCount: value.generatedTokenCount };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Copy measurements while the generation still owns the session, before reset. */
|
|
44
|
+
export function readWorkGenerationTiming(stats) {
|
|
45
|
+
if (!stats || !Number.isFinite(stats.totalTimeMs) || stats.totalTimeMs <= 0
|
|
46
|
+
|| !Number.isSafeInteger(stats.tokensGenerated) || stats.tokensGenerated < 0) return null;
|
|
47
|
+
const duration = value => Number.isFinite(value) && value >= 0 ? value : null;
|
|
48
|
+
return { totalMs: stats.totalTimeMs, generatedTokenCount: stats.tokensGenerated,
|
|
49
|
+
prefillMs: duration(stats.prefillTimeMs), decodeMs: duration(stats.decodeTimeMs),
|
|
50
|
+
firstTokenMs: duration(stats.ttftMs),
|
|
51
|
+
...(Number.isSafeInteger(stats.reusedPrefixTokens) && stats.reusedPrefixTokens >= 0
|
|
52
|
+
? { reusedPrefixTokens: stats.reusedPrefixTokens } : {}),
|
|
53
|
+
...(Number.isSafeInteger(stats.prefillComputedTokens) && stats.prefillComputedTokens >= 0
|
|
54
|
+
? { prefillComputedTokens: stats.prefillComputedTokens } : {}) };
|
|
55
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/** Host-selected compatibility providers. Signed Pack providers remain separate. */
|
|
2
|
+
import profile from '../config/work-profile.json' with { type: 'json' };
|
|
3
|
+
import { createGenerationContextCache } from '../../../shared/vendor/reploid/mesh/generation-context-cache.js';
|
|
4
|
+
import { publicProviderHardware } from '../../../shared/vendor/reploid/mesh/provider-hardware.js';
|
|
5
|
+
import { consumeWorkGenerationStream, readWorkGenerationCompletion, readWorkGenerationTiming } from './work-generation-completion.js';
|
|
6
|
+
const requireValue = (condition, message) => { if (!condition) throw new Error(message); };
|
|
7
|
+
|
|
8
|
+
export function selectWorkModel({ models, modelId, defaultModelId }) {
|
|
9
|
+
const id = modelId === undefined || modelId === null || modelId === '' ? defaultModelId : modelId;
|
|
10
|
+
const model = models.find(item => item.id === id);
|
|
11
|
+
requireValue(model, `Unknown application model: ${String(id)}`);
|
|
12
|
+
requireValue(['doppler', 'gemini'].includes(model.provider), 'Model requires an explicit supported provider');
|
|
13
|
+
return Object.freeze({ ...model });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function openWorkProvider({ model, service, scope, signal, credentials,
|
|
17
|
+
fetchImpl = globalThis.fetch, generation, maxOutcomeCharacters, allowedFallbackModels = [], onProgress,
|
|
18
|
+
resolveAdapter, source = model.id, loadOptions = {} }) {
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
generation = { ...generation, ...model.generation };
|
|
21
|
+
const session = model.provider === 'doppler'
|
|
22
|
+
? await service.open({ scope, source, options: { ...model.loadOptions, ...loadOptions, onProgress } }) : null;
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
if (session) requireValue(typeof session.stream === 'function', 'Configured Doppler session does not expose text streaming');
|
|
25
|
+
else requireValue(typeof credentials === 'function', 'Firebase Auth and App Check credentials are required');
|
|
26
|
+
let healthy = true;
|
|
27
|
+
const configuredBufferBudget = (loadOptions?.runtimeConfig ?? model.loadOptions?.runtimeConfig)?.shared?.bufferPool?.budget?.maxTotalBytes;
|
|
28
|
+
const contextCachePolicy = Number.isFinite(configuredBufferBudget) && configuredBufferBudget > 0
|
|
29
|
+
? (() => {
|
|
30
|
+
const maxBytes = Math.min(profile.contextCache.maxBytes, Math.max(67108864, Math.floor(configuredBufferBudget * 0.25)));
|
|
31
|
+
return { ...profile.contextCache, maxBytes, maxEntryBytes: Math.min(profile.contextCache.maxEntryBytes, maxBytes) };
|
|
32
|
+
})()
|
|
33
|
+
: profile.contextCache;
|
|
34
|
+
const contexts = session?.supports?.('generationContext') === true
|
|
35
|
+
? createGenerationContextCache({ policy: contextCachePolicy, create: options => session.createGenerationContext(options) }) : null;
|
|
36
|
+
const executionIdentity = () => {
|
|
37
|
+
if (!session) return null;
|
|
38
|
+
requireValue(healthy, 'Doppler session requires replacement after failed cleanup');
|
|
39
|
+
requireValue(session.loaded === true && typeof session.modelId === 'string', 'Doppler session is not ready');
|
|
40
|
+
const hash = String(session.manifestHash || '').replace(/^sha256:/, '');
|
|
41
|
+
requireValue(/^[a-f0-9]{64}$/.test(hash), 'Doppler session is missing its manifest identity');
|
|
42
|
+
requireValue(session.modelId === model.id, 'Doppler loaded a different model');
|
|
43
|
+
requireValue(!model.identity || model.identity === 'sha256:' + hash, 'Doppler loaded a different model artifact');
|
|
44
|
+
requireValue(!session.activeLoRA, 'Active adapter identity requires Doppler generation evidence');
|
|
45
|
+
return { model: session.modelId, modelIdentity: 'sha256:' + hash,
|
|
46
|
+
adapterIdentities: [] };
|
|
47
|
+
};
|
|
48
|
+
const evidencedIdentity = (result, adapters) => {
|
|
49
|
+
const actual = result?.evidence?.runtimeProfile?.model;
|
|
50
|
+
const hash = String(actual?.manifestHash || '').replace(/^sha256:/, '');
|
|
51
|
+
requireValue(actual?.modelId === model.id && 'sha256:' + hash === model.identity,
|
|
52
|
+
'Doppler generation evidence has a different model artifact');
|
|
53
|
+
const identities = actual.activeAdapterDigest ? [actual.activeAdapterDigest] : [];
|
|
54
|
+
requireValue(JSON.stringify(identities) === JSON.stringify(adapters.map(item => item.identity)),
|
|
55
|
+
'Doppler generation evidence has a different adapter artifact');
|
|
56
|
+
return { model: actual.modelId, modelIdentity: 'sha256:' + hash, adapterIdentities: identities };
|
|
57
|
+
};
|
|
58
|
+
if (session) executionIdentity();
|
|
59
|
+
const reset = async () => {
|
|
60
|
+
if (!session) return;
|
|
61
|
+
requireValue(typeof session.resetGenerationState === 'function', 'Doppler session cannot reset conversation state');
|
|
62
|
+
try { await session.resetGenerationState(); }
|
|
63
|
+
catch (error) { healthy = false; throw error; }
|
|
64
|
+
};
|
|
65
|
+
const setAdapters = async (adapters, attemptSignal = signal) => {
|
|
66
|
+
requireValue(Array.isArray(adapters) && adapters.length <= 1, 'This Doppler session supports one adapter at a time');
|
|
67
|
+
const selected = adapters[0];
|
|
68
|
+
if (selected) {
|
|
69
|
+
requireValue(selected.baseModelIdentity === model.identity, 'Adapter targets a different base model artifact');
|
|
70
|
+
requireValue(/^sha256:[a-f0-9]{64}$/.test(selected.identity), 'Exact adapter execution identity required');
|
|
71
|
+
requireValue(typeof resolveAdapter === 'function', 'Verified adapter acquisition is unavailable');
|
|
72
|
+
}
|
|
73
|
+
requireValue(session, 'Adapters require a Doppler session');
|
|
74
|
+
if (session.activeLoRA) await session.unloadLoRA();
|
|
75
|
+
requireValue(!session.activeLoRA, 'Doppler did not remove the previous adapter');
|
|
76
|
+
if (!selected) return;
|
|
77
|
+
attemptSignal.throwIfAborted();
|
|
78
|
+
const acquired = await resolveAdapter(structuredClone(selected), { signal: attemptSignal });
|
|
79
|
+
attemptSignal.throwIfAborted();
|
|
80
|
+
requireValue(acquired?.manifest?.baseModel === model.id, 'Adapter manifest targets a different model');
|
|
81
|
+
await session.loadLoRA(acquired.manifest, acquired.options);
|
|
82
|
+
attemptSignal.throwIfAborted();
|
|
83
|
+
requireValue(session.activeLoRA, 'Doppler did not apply the adapter');
|
|
84
|
+
};
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
getIdentity: executionIdentity,
|
|
87
|
+
getHardware: () => publicProviderHardware(session?.deviceInfo),
|
|
88
|
+
getContextCacheStats: () => contexts?.getStats?.() ?? null,
|
|
89
|
+
isReady: () => healthy && session?.loaded === true,
|
|
90
|
+
reset,
|
|
91
|
+
close: () => contexts?.close(),
|
|
92
|
+
async prepareAdapters(adapters) {
|
|
93
|
+
try {
|
|
94
|
+
await setAdapters(adapters);
|
|
95
|
+
// The pinned scoped API reports tensor identity in execution evidence, not a session getter.
|
|
96
|
+
const result = await session.generate(profile.adapterQualification.messages,
|
|
97
|
+
{ ...profile.adapterQualification.generation, signal });
|
|
98
|
+
signal.throwIfAborted();
|
|
99
|
+
return evidencedIdentity(result, adapters).adapterIdentities;
|
|
100
|
+
} finally {
|
|
101
|
+
try { await setAdapters([]); await reset(); }
|
|
102
|
+
catch (error) { healthy = false; throw error; }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
async generate(messages, onUpdate = () => {}, { signal: attemptSignal, adapters = [], maxOutputTokens, contextOwner, contextId }) {
|
|
106
|
+
const combined = AbortSignal.any([signal, attemptSignal]);
|
|
107
|
+
combined.throwIfAborted();
|
|
108
|
+
requireValue(maxOutputTokens === undefined || Number.isSafeInteger(maxOutputTokens) && maxOutputTokens > 0,
|
|
109
|
+
'Request maxOutputTokens must be a positive integer');
|
|
110
|
+
let requestGeneration = maxOutputTokens === undefined ? generation
|
|
111
|
+
: { ...generation, maxTokens: Math.min(generation.maxTokens ?? maxOutputTokens, maxOutputTokens) };
|
|
112
|
+
let text = '', actualModel = model.id, actualIdentity = null, completion = null, timing = null;
|
|
113
|
+
const update = delta => {
|
|
114
|
+
combined.throwIfAborted();
|
|
115
|
+
requireValue(typeof delta === 'string', 'Provider emitted an invalid text delta');
|
|
116
|
+
text += delta;
|
|
117
|
+
requireValue(text.length <= maxOutcomeCharacters, 'Model response exceeded the configured size limit');
|
|
118
|
+
return onUpdate(delta);
|
|
119
|
+
};
|
|
120
|
+
if (session) {
|
|
121
|
+
const advanced = session.supports?.('advanced') === false ? null : session.advanced;
|
|
122
|
+
if (typeof advanced?.getGenerationGeometry === 'function' && typeof advanced?.tokenizePrompt === 'function') {
|
|
123
|
+
const maxSeqLen = advanced.getGenerationGeometry()?.maxSeqLen;
|
|
124
|
+
if (Number.isSafeInteger(maxSeqLen) && maxSeqLen > 0) {
|
|
125
|
+
const promptTokens = advanced.tokenizePrompt(messages, { useChatTemplate: requestGeneration.useChatTemplate !== false }).length;
|
|
126
|
+
const requiredOutputTokens = Number.isSafeInteger(requestGeneration.maxTokens) && requestGeneration.maxTokens > 0
|
|
127
|
+
? requestGeneration.maxTokens : 1;
|
|
128
|
+
if (promptTokens + requiredOutputTokens > maxSeqLen) {
|
|
129
|
+
throw Object.assign(
|
|
130
|
+
new Error(`Conversation plus output budget exceeds model context limit (${promptTokens} + ${requiredOutputTokens} > ${maxSeqLen} tokens)`),
|
|
131
|
+
{ code: 'CONTEXT_LENGTH_EXCEEDED', recoverableSession: true, status: 400 }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Raw compatibility sessions cancel cooperatively; the host waits for settlement.
|
|
137
|
+
await reset();
|
|
138
|
+
let lease = null, complete = false;
|
|
139
|
+
try {
|
|
140
|
+
await setAdapters(adapters, combined);
|
|
141
|
+
const contextKey = typeof contextOwner === 'string' && contextOwner && typeof contextId === 'string' && contextId
|
|
142
|
+
? JSON.stringify([contextOwner, contextId, model.identity, adapters.map(item => item.identity)]) : null;
|
|
143
|
+
lease = contexts?.acquire(contextKey);
|
|
144
|
+
const cachedGeneration = lease ? { ...requestGeneration, generationContext: lease.context } : requestGeneration;
|
|
145
|
+
if (adapters.length) {
|
|
146
|
+
const result = await session.generate(messages, { ...cachedGeneration, signal: combined,
|
|
147
|
+
onToken: (_token, delta) => { if (typeof delta === 'string') update(delta); } });
|
|
148
|
+
// Doppler owns visible output. Older callbacks supply only token IDs;
|
|
149
|
+
// those sessions deliver completion text without decoding IDs here.
|
|
150
|
+
requireValue(typeof result.outputText === 'string' && result.outputText.startsWith(text),
|
|
151
|
+
'Doppler streamed text differs from generation evidence');
|
|
152
|
+
await update(result.outputText.slice(text.length));
|
|
153
|
+
actualIdentity = evidencedIdentity(result, adapters);
|
|
154
|
+
completion = readWorkGenerationCompletion(result);
|
|
155
|
+
timing = readWorkGenerationTiming(result.evidence?.stats);
|
|
156
|
+
requireValue(text === result.outputText, 'Doppler streamed text differs from generation evidence');
|
|
157
|
+
} else {
|
|
158
|
+
actualIdentity = executionIdentity();
|
|
159
|
+
const result = await consumeWorkGenerationStream(session.stream(messages, { ...cachedGeneration, signal: combined }),
|
|
160
|
+
{ signal: combined, onDelta: update, maxOutcomeCharacters });
|
|
161
|
+
completion = result.completion;
|
|
162
|
+
timing = readWorkGenerationTiming(session.supports?.('advanced') === false
|
|
163
|
+
? null : session.advanced?.getStats?.());
|
|
164
|
+
requireValue(JSON.stringify(executionIdentity()) === JSON.stringify(actualIdentity),
|
|
165
|
+
'Doppler execution identity changed during generation');
|
|
166
|
+
}
|
|
167
|
+
combined.throwIfAborted();
|
|
168
|
+
complete = true;
|
|
169
|
+
} finally {
|
|
170
|
+
try { lease?.release({ discard: !complete }); await reset(); await setAdapters([]); }
|
|
171
|
+
catch (error) { healthy = false; throw error; }
|
|
172
|
+
}
|
|
173
|
+
actualModel = actualIdentity.model;
|
|
174
|
+
} else {
|
|
175
|
+
const headers = new Headers(await credentials({ signal: combined }));
|
|
176
|
+
requireValue(/^Bearer\s+\S+$/i.test(headers.get('Authorization') || '')
|
|
177
|
+
&& !!headers.get('X-Firebase-AppCheck')?.trim(), 'Firebase Auth and App Check credentials are required');
|
|
178
|
+
headers.set('Content-Type', 'application/json');
|
|
179
|
+
combined.throwIfAborted();
|
|
180
|
+
const response = await fetchImpl('/zero/gemini', {
|
|
181
|
+
method: 'POST', headers,
|
|
182
|
+
body: JSON.stringify({ model: model.id, provider: model.provider, messages,
|
|
183
|
+
allowedFallbackModels, maxOutputTokens: requestGeneration.maxTokens }),
|
|
184
|
+
signal: combined
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
const data = await response.json().catch(() => ({}));
|
|
188
|
+
const error = new Error(`Gemini HTTP ${response.status}: ${data.error || response.statusText}`);
|
|
189
|
+
error.status = response.status;
|
|
190
|
+
error.retryAfter = response.headers.get('retry-after');
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
actualModel = data.model || data.effectiveModel;
|
|
195
|
+
requireValue(typeof actualModel === 'string' && actualModel,
|
|
196
|
+
'Provider response is missing execution model identity');
|
|
197
|
+
requireValue(actualModel === model.id || allowedFallbackModels.includes(actualModel),
|
|
198
|
+
'Provider substituted a model outside the declared fallback policy');
|
|
199
|
+
requireValue(!data.provider || data.provider === model.provider, 'Provider identity mismatch');
|
|
200
|
+
await update(data.content);
|
|
201
|
+
}
|
|
202
|
+
combined.throwIfAborted();
|
|
203
|
+
requireValue(text.trim() || completion?.stopReason === 'max-tokens', 'Provider returned no text');
|
|
204
|
+
return { content: text, raw: text, requestedModel: model.id, model: actualModel,
|
|
205
|
+
...(session ? { ...actualIdentity, completion, timing, hardware: publicProviderHardware(session.deviceInfo) } : {}),
|
|
206
|
+
provider: model.provider, execution: session ? 'local-scoped-session' : 'cloud-proxy-session' };
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|