@oxyhq/contracts 0.29.0 → 0.30.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/identity.js +33 -1
- package/dist/cjs/index.js +20 -7
- package/dist/cjs/inference/aliaModelRelease.js +250 -0
- package/dist/cjs/inference/catalogue.js +1 -4
- package/dist/cjs/inference/identifiers.js +23 -3
- package/dist/cjs/inference/request.js +188 -5
- package/dist/cjs/inference/routingPolicy.js +81 -2
- package/dist/cjs/inference/streamEvents.js +27 -0
- package/dist/cjs/inference/usage.js +32 -1
- package/dist/cjs/inference/version.js +15 -2
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/identity.js +32 -0
- package/dist/esm/index.js +8 -3
- package/dist/esm/inference/aliaModelRelease.js +247 -0
- package/dist/esm/inference/catalogue.js +2 -5
- package/dist/esm/inference/identifiers.js +22 -2
- package/dist/esm/inference/request.js +189 -6
- package/dist/esm/inference/routingPolicy.js +81 -2
- package/dist/esm/inference/streamEvents.js +27 -0
- package/dist/esm/inference/usage.js +32 -1
- package/dist/esm/inference/version.js +15 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/identity.d.ts +59 -1
- package/dist/types/index.d.ts +7 -5
- package/dist/types/inference/accountBilling.d.ts +26 -26
- package/dist/types/inference/aliaModelRelease.d.ts +597 -0
- package/dist/types/inference/catalogue.d.ts +6 -6
- package/dist/types/inference/entitlement.d.ts +4 -4
- package/dist/types/inference/identifiers.d.ts +20 -2
- package/dist/types/inference/money.d.ts +4 -4
- package/dist/types/inference/priceVersion.d.ts +14 -14
- package/dist/types/inference/providerConnection.d.ts +4 -4
- package/dist/types/inference/request.d.ts +291 -5
- package/dist/types/inference/routingPolicy.d.ts +109 -13
- package/dist/types/inference/streamEvents.d.ts +53 -26
- package/dist/types/inference/usage.d.ts +103 -79
- package/dist/types/inference/version.d.ts +15 -2
- package/dist/types/keyRecovery.d.ts +4 -4
- package/package.json +1 -1
package/dist/esm/identity.js
CHANGED
|
@@ -204,6 +204,37 @@ export const exportAttestationSchema = z.object({
|
|
|
204
204
|
signature: z.string(),
|
|
205
205
|
signedAt: z.number(),
|
|
206
206
|
});
|
|
207
|
+
export const exportUsageReceiptSchema = z.object({
|
|
208
|
+
receiptId: z.string(),
|
|
209
|
+
requestId: z.string(),
|
|
210
|
+
settledAt: z.string(),
|
|
211
|
+
billedAmount: z.string(),
|
|
212
|
+
currency: z.string(),
|
|
213
|
+
outcome: z.string(),
|
|
214
|
+
resolvedModelReference: z.string(),
|
|
215
|
+
servingProvider: z.string(),
|
|
216
|
+
platformFeeOnly: z.boolean(),
|
|
217
|
+
});
|
|
218
|
+
export const exportLedgerEntrySchema = z.object({
|
|
219
|
+
entryId: z.string(),
|
|
220
|
+
kind: z.string(),
|
|
221
|
+
currency: z.string(),
|
|
222
|
+
createdAt: z.string(),
|
|
223
|
+
});
|
|
224
|
+
export const exportUsageReservationSchema = z.object({
|
|
225
|
+
reservationId: z.string(),
|
|
226
|
+
requestId: z.string(),
|
|
227
|
+
status: z.string(),
|
|
228
|
+
reservedAmount: z.string(),
|
|
229
|
+
currency: z.string(),
|
|
230
|
+
createdAt: z.string(),
|
|
231
|
+
expiresAt: z.string(),
|
|
232
|
+
});
|
|
233
|
+
export const exportFinancialSectionSchema = z.object({
|
|
234
|
+
receipts: z.array(exportUsageReceiptSchema),
|
|
235
|
+
ledgerEntries: z.array(exportLedgerEntrySchema),
|
|
236
|
+
reservations: z.array(exportUsageReservationSchema),
|
|
237
|
+
});
|
|
207
238
|
export const exportBundleSchema = z.object({
|
|
208
239
|
'$schema': z.string(),
|
|
209
240
|
exportedAt: z.string(),
|
|
@@ -218,6 +249,7 @@ export const exportBundleSchema = z.object({
|
|
|
218
249
|
following: z.array(z.string()),
|
|
219
250
|
followers: z.array(z.string()),
|
|
220
251
|
}),
|
|
252
|
+
financial: exportFinancialSectionSchema,
|
|
221
253
|
attestation: exportAttestationSchema.nullable(),
|
|
222
254
|
proof: exportAttestationSchema.optional(),
|
|
223
255
|
});
|
package/dist/esm/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export {
|
|
|
30
30
|
recommendationExcludeTypeSchema, recommendationBoostSchema, recommendationSignalWeightsSchema, recommendationRequestSchema, recommendationCountSchema, recommendationItemSchema, recommendationResponseSchema, appEndorsementInputSchema, appInterestInputSchema, appUserSignalIngestSchema, appAffinityEventTypeSchema, appAffinityEventSchema, appAffinityEventsIngestSchema, } from './recommendations.js';
|
|
31
31
|
export {
|
|
32
32
|
// Schemas
|
|
33
|
-
verificationMethodSchema, didServiceSchema, didDocumentSchema, signedRecordEnvelopeSchema, verifiedDomainSchema, domainVerificationRequestSchema, domainVerificationInstructionsSchema, authMethodEntrySchema, authMethodsResponseSchema, exportAttestationSchema, exportBundleSchema, } from './identity.js';
|
|
33
|
+
verificationMethodSchema, didServiceSchema, didDocumentSchema, signedRecordEnvelopeSchema, verifiedDomainSchema, domainVerificationRequestSchema, domainVerificationInstructionsSchema, authMethodEntrySchema, authMethodsResponseSchema, exportAttestationSchema, exportUsageReceiptSchema, exportLedgerEntrySchema, exportUsageReservationSchema, exportFinancialSectionSchema, exportBundleSchema, } from './identity.js';
|
|
34
34
|
export {
|
|
35
35
|
// Schemas
|
|
36
36
|
oxySignedRecordTypeSchema, } from './oxyRecordTypes.js';
|
|
@@ -113,7 +113,7 @@ export {
|
|
|
113
113
|
// are branded apart so a delegated end user can never become the payer.
|
|
114
114
|
oxyAccountIdSchema, delegatedUserIdSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, generationIdSchema, idempotencyKeySchema, inferenceEnvironmentSchema,
|
|
115
115
|
// Wire primitives
|
|
116
|
-
inferenceTimestampSchema, inferenceDateSchema, inferenceHttpsUrlSchema,
|
|
116
|
+
inferenceTimestampSchema, inferenceDateSchema, inferenceHttpsUrlSchema, sha256DigestSchema,
|
|
117
117
|
// Catalogue references
|
|
118
118
|
publisherSlugSchema, modelSlugSchema, modelIdSchema, modelRevisionLabelSchema, modelReferenceSchema, routingProfileSlugSchema, inferenceProviderSlugSchema, deploymentIdSchema, inferenceRegionSchema, RESERVED_ALIA_PUBLISHER, } from './inference/identifiers.js';
|
|
119
119
|
export {
|
|
@@ -134,7 +134,12 @@ inferenceModalitySchema, modelCapabilitiesSchema, modelLicenseSchema, modelProve
|
|
|
134
134
|
export {
|
|
135
135
|
// Routing policy: every control, plus the refinement that rejects a policy
|
|
136
136
|
// no route could ever satisfy.
|
|
137
|
-
routingTargetSchema, routingPolicyScopeSchema, routingFallbackPolicySchema, routingPolicySchema, routingPolicyReferenceSchema,
|
|
137
|
+
routingTargetSchema, routingPolicyScopeSchema, routingFallbackPolicySchema, routingPolicySchema, routingPolicyReferenceSchema,
|
|
138
|
+
// What the data plane actually receives: the routes the policy authorized.
|
|
139
|
+
authorizedRouteSchema, } from './inference/routingPolicy.js';
|
|
140
|
+
export {
|
|
141
|
+
// The signed Alia model release manifest (ingestion contract; no endpoint).
|
|
142
|
+
aliaReleaseArtifactSchema, aliaReleaseSignatureSchema, aliaModelReleaseManifestSchema, } from './inference/aliaModelRelease.js';
|
|
138
143
|
export {
|
|
139
144
|
// The normalized Oxy→data-plane request envelope.
|
|
140
145
|
inferenceContentSourceSchema, inferenceContentPartSchema, inferenceToolCallSchema, inferenceMessageRoleSchema, inferenceMessageSchema, inferenceInputSchema, samplingParametersSchema, toolDefinitionSchema, toolChoiceSchema, responseFormatSchema, clientRequestMetadataSchema, inferenceRequestSchema, } from './inference/request.js';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed Alia model release manifest — the ingestion contract for a
|
|
3
|
+
* first-party model release.
|
|
4
|
+
*
|
|
5
|
+
* The catalogue already STORES everything such a manifest carries: the model
|
|
6
|
+
* card, the licence block, the provenance and base model, the evaluation table,
|
|
7
|
+
* the safety metadata, and an artifact digest with a `sha256:<64 hex>` CHECK.
|
|
8
|
+
* What did not exist was the manifest itself — a single document Alia SIGNS,
|
|
9
|
+
* asserting all of it at once — and that is the gap this shape closes. Nothing
|
|
10
|
+
* here re-declares a catalogue field; the manifest COMPOSES the published shapes
|
|
11
|
+
* so a manifest and the catalogue row it produces cannot describe a release
|
|
12
|
+
* differently.
|
|
13
|
+
*
|
|
14
|
+
* ## The manifest tightens the revision it carries
|
|
15
|
+
*
|
|
16
|
+
* `modelRevisionSchema` makes `modelCardUrl`, `artifactDigest`, `evaluations`
|
|
17
|
+
* and `safety` optional, because a third-party route legitimately has none of
|
|
18
|
+
* them — Oxy did not train those weights and cannot publish a card for them. A
|
|
19
|
+
* FIRST-PARTY release has no such excuse: the documentation trail is the reason
|
|
20
|
+
* a release manifest exists at all, and a model Alia ships without one is not a
|
|
21
|
+
* release, it is a deployment. So the refinement below requires all four,
|
|
22
|
+
* without changing the catalogue shape that a third-party entry still parses
|
|
23
|
+
* through.
|
|
24
|
+
*
|
|
25
|
+
* ## `.strict()` at the top level, and here that is forced rather than chosen
|
|
26
|
+
*
|
|
27
|
+
* The shapes exchanged with the data plane tolerate an unknown field, because
|
|
28
|
+
* refusing a producer one minor version ahead is a worse failure than ignoring
|
|
29
|
+
* its addition (`version.ts`). A SIGNED document inverts that: the signature is
|
|
30
|
+
* over the canonical bytes of the manifest, so a field silently stripped at this
|
|
31
|
+
* parse is a field missing from the bytes Oxy re-canonicalizes, and verification
|
|
32
|
+
* fails. A tolerant parse would therefore report "the signature is invalid" for
|
|
33
|
+
* what is really "this build does not understand this manifest" — the wrong
|
|
34
|
+
* diagnosis of the right problem. Strict says the true thing, and the cost is
|
|
35
|
+
* bounded: ingestion is a release-time operation an operator retries once Oxy
|
|
36
|
+
* takes the newer contract, not a served request that becomes unsettleable.
|
|
37
|
+
*
|
|
38
|
+
* ## What is deliberately NOT here
|
|
39
|
+
*
|
|
40
|
+
* **No HTTP write path.** The catalogue's emptiness is currently a safety
|
|
41
|
+
* property — `scripts/seed-inference-catalogue.ts` refuses to invent a licence
|
|
42
|
+
* or a retention flag because "a plausible invented value in a catalogue is
|
|
43
|
+
* worse than an absent one" — and a staff write path into it with nothing to
|
|
44
|
+
* ingest is an unexercised hazard. The schema lands; the endpoint waits for a
|
|
45
|
+
* real manifest to ingest.
|
|
46
|
+
*
|
|
47
|
+
* **No `payloadDigest` field.** The signature is over the canonical
|
|
48
|
+
* serialization of this manifest with `signatures` removed, and a verifier
|
|
49
|
+
* recomputes it. Storing the digest beside the document it digests would be a
|
|
50
|
+
* second source of truth for one fact, and a verifier that compared the
|
|
51
|
+
* signature against the DECLARED digest rather than the recomputed one would
|
|
52
|
+
* verify nothing at all.
|
|
53
|
+
*
|
|
54
|
+
* **No verification RESULT.** Whether a signature checked out is Oxy's finding
|
|
55
|
+
* about the document, not a claim the document makes about itself; a `verified`
|
|
56
|
+
* field inside a signed manifest is the signer asserting its own signature.
|
|
57
|
+
*
|
|
58
|
+
* ## The open owner decision this shape does not take
|
|
59
|
+
*
|
|
60
|
+
* **What signs, and what verifies, is not decided.** Oxy holds no Alia signing
|
|
61
|
+
* key, and whether to resolve `keyId` through the existing attestation machinery
|
|
62
|
+
* (`services/oxyVerificationResolver.ts`, the civic attestation code) or to
|
|
63
|
+
* introduce a dedicated Alia release key is a real choice with different
|
|
64
|
+
* custody, rotation and revocation consequences. So `keyId` is an OPAQUE
|
|
65
|
+
* identifier and this file names no registry that resolves it: either answer
|
|
66
|
+
* fits, and neither is presupposed. Until it is answered a manifest can be
|
|
67
|
+
* parsed and cannot be verified, which is the second reason no endpoint ships.
|
|
68
|
+
*
|
|
69
|
+
* Decided in: docs/adr/0008-catalogue-concept-separation.md,
|
|
70
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md, issue #972 §12.
|
|
71
|
+
*/
|
|
72
|
+
import { z } from 'zod';
|
|
73
|
+
import { modelLicenseSchema, modelProvenanceSchema, modelRevisionSchema, } from './catalogue.js';
|
|
74
|
+
import { inferenceTimestampSchema, RESERVED_ALIA_PUBLISHER, sha256DigestSchema, } from './identifiers.js';
|
|
75
|
+
/**
|
|
76
|
+
* One artifact of a release, by path and digest.
|
|
77
|
+
*
|
|
78
|
+
* `sizeBytes` is required beside the digest so a verifier can refuse a stream
|
|
79
|
+
* that is the wrong length before reading it to the end, rather than only after.
|
|
80
|
+
*/
|
|
81
|
+
export const aliaReleaseArtifactSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
/** Path within the release, e.g. `model-00001-of-00004.safetensors`. */
|
|
84
|
+
path: z.string().min(1).max(512),
|
|
85
|
+
digest: sha256DigestSchema,
|
|
86
|
+
sizeBytes: z.number().int().positive().safe(),
|
|
87
|
+
mediaType: z.string().min(1).max(255).optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
/**
|
|
91
|
+
* One detached signature over the manifest.
|
|
92
|
+
*
|
|
93
|
+
* `algorithm` is a CLOSED enum with one member, and both halves of that are
|
|
94
|
+
* deliberate. Closed, because a verifier that trusts a document's own algorithm
|
|
95
|
+
* name accepts whatever that document nominates, `none` included. One member,
|
|
96
|
+
* because Ed25519 is the scheme ADR 0012 already chose for asymmetric
|
|
97
|
+
* verification on this platform, and naming a scheme nothing here can check
|
|
98
|
+
* would be advertising a capability that does not exist. A second member lands
|
|
99
|
+
* when a verifier for it does — which is a closed enum gaining a member, and
|
|
100
|
+
* therefore a MINOR contract-set change the handshake surfaces (`version.ts`).
|
|
101
|
+
*
|
|
102
|
+
* `keyId` is opaque on purpose: see the header. It identifies the public key
|
|
103
|
+
* without saying what resolves it.
|
|
104
|
+
*
|
|
105
|
+
* The signature covers the canonical serialization (RFC 8785 JCS) of the
|
|
106
|
+
* manifest with `signatures` removed. The canonicalization is NAMED rather than
|
|
107
|
+
* left implicit because a digest over "the manifest" is not verifiable by two
|
|
108
|
+
* implementations that serialize JSON differently; naming it is a mechanical
|
|
109
|
+
* necessity and is independent of the open question of which key signs.
|
|
110
|
+
*/
|
|
111
|
+
export const aliaReleaseSignatureSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
algorithm: z.enum(['ed25519']),
|
|
114
|
+
canonicalization: z.enum(['jcs']),
|
|
115
|
+
/** Opaque identifier of the public key. Resolving it is undecided. */
|
|
116
|
+
keyId: z.string().min(1).max(256),
|
|
117
|
+
/**
|
|
118
|
+
* Unpadded base64url. Exactly 86 characters, which is a 64-byte Ed25519
|
|
119
|
+
* signature — the one algorithm above. A second algorithm moves this length
|
|
120
|
+
* into a refinement keyed on `algorithm`.
|
|
121
|
+
*/
|
|
122
|
+
signature: z
|
|
123
|
+
.string()
|
|
124
|
+
.regex(/^[A-Za-z0-9_-]{86}$/, 'signature must be a 64-byte ed25519 signature in unpadded base64url'),
|
|
125
|
+
signedAt: inferenceTimestampSchema,
|
|
126
|
+
})
|
|
127
|
+
.strict();
|
|
128
|
+
/**
|
|
129
|
+
* A signed release of an `alia/*` model revision.
|
|
130
|
+
*
|
|
131
|
+
* `signatures` is a LIST rather than one signature, because "what signs" is
|
|
132
|
+
* undecided: a single field would presuppose one signer, while a list lets an
|
|
133
|
+
* Alia release key and an existing attestation co-sign the same document without
|
|
134
|
+
* either being retrofitted later.
|
|
135
|
+
*/
|
|
136
|
+
export const aliaModelReleaseManifestSchema = z
|
|
137
|
+
.object({
|
|
138
|
+
/** See `version.ts`: an ingestion payload is a whole message on the wire. */
|
|
139
|
+
schemaVersion: z.literal(1),
|
|
140
|
+
/** The release's own identity, so ingestion is idempotent on it. */
|
|
141
|
+
releaseId: z.string().min(1).max(128),
|
|
142
|
+
issuedAt: inferenceTimestampSchema,
|
|
143
|
+
/**
|
|
144
|
+
* The revision being released. Carries its OWN `schemaVersion`, like
|
|
145
|
+
* `billingProfileSchema` inside `accountBillingStateSchema`: the manifest's
|
|
146
|
+
* version governs the manifest and the revision's governs the revision,
|
|
147
|
+
* which is two versions of two things rather than two versions of one.
|
|
148
|
+
*/
|
|
149
|
+
revision: modelRevisionSchema,
|
|
150
|
+
/** On the MODEL rather than the revision in the catalogue, so carried here. */
|
|
151
|
+
provenance: modelProvenanceSchema,
|
|
152
|
+
license: modelLicenseSchema,
|
|
153
|
+
artifacts: z.array(aliaReleaseArtifactSchema).min(1),
|
|
154
|
+
signatures: z.array(aliaReleaseSignatureSchema).min(1),
|
|
155
|
+
})
|
|
156
|
+
.strict()
|
|
157
|
+
.superRefine((manifest, ctx) => {
|
|
158
|
+
// The same rule `catalogueModelSchema` enforces on a model, applied to the
|
|
159
|
+
// carrier that creates one: `alia/*` names models Alia actually owns or
|
|
160
|
+
// derived, and a manifest is the document that would launder somebody else's
|
|
161
|
+
// weights into the namespace.
|
|
162
|
+
const publisher = manifest.revision.modelId.slice(0, manifest.revision.modelId.indexOf('/'));
|
|
163
|
+
if (publisher !== RESERVED_ALIA_PUBLISHER) {
|
|
164
|
+
ctx.addIssue({
|
|
165
|
+
code: z.ZodIssueCode.custom,
|
|
166
|
+
path: ['revision', 'modelId'],
|
|
167
|
+
message: `an Alia release manifest releases a ${RESERVED_ALIA_PUBLISHER}/* model`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (manifest.provenance.releaseKind !== 'first_party_original' &&
|
|
171
|
+
manifest.provenance.releaseKind !== 'first_party_derived') {
|
|
172
|
+
ctx.addIssue({
|
|
173
|
+
code: z.ZodIssueCode.custom,
|
|
174
|
+
path: ['provenance', 'releaseKind'],
|
|
175
|
+
message: 'an Alia release manifest describes a first-party release',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// A derived model's base is the licence-attribution trail. Recording the
|
|
179
|
+
// derivation without naming what it derives from loses exactly the fact
|
|
180
|
+
// attribution needs.
|
|
181
|
+
if (manifest.provenance.releaseKind === 'first_party_derived' &&
|
|
182
|
+
manifest.provenance.baseModelId === undefined) {
|
|
183
|
+
ctx.addIssue({
|
|
184
|
+
code: z.ZodIssueCode.custom,
|
|
185
|
+
path: ['provenance', 'baseModelId'],
|
|
186
|
+
message: 'a derived release names the model it derives from',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// The four fields a third-party catalogue entry may omit and a first-party
|
|
190
|
+
// release may not. See the header.
|
|
191
|
+
if (manifest.revision.modelCardUrl === undefined) {
|
|
192
|
+
ctx.addIssue({
|
|
193
|
+
code: z.ZodIssueCode.custom,
|
|
194
|
+
path: ['revision', 'modelCardUrl'],
|
|
195
|
+
message: 'a first-party release publishes a model card',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (manifest.revision.safety === undefined) {
|
|
199
|
+
ctx.addIssue({
|
|
200
|
+
code: z.ZodIssueCode.custom,
|
|
201
|
+
path: ['revision', 'safety'],
|
|
202
|
+
message: 'a first-party release publishes its safety metadata',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (manifest.revision.evaluations.length === 0) {
|
|
206
|
+
ctx.addIssue({
|
|
207
|
+
code: z.ZodIssueCode.custom,
|
|
208
|
+
path: ['revision', 'evaluations'],
|
|
209
|
+
message: 'a first-party release publishes at least one evaluation result',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// The digest the catalogue will serve has to be one of the digests this
|
|
213
|
+
// manifest signed. Otherwise the signature covers a set of artifacts that
|
|
214
|
+
// does not include the weights anybody runs.
|
|
215
|
+
if (manifest.revision.artifactDigest === undefined) {
|
|
216
|
+
ctx.addIssue({
|
|
217
|
+
code: z.ZodIssueCode.custom,
|
|
218
|
+
path: ['revision', 'artifactDigest'],
|
|
219
|
+
message: 'a first-party release names the digest of the artifact it serves',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else if (!manifest.artifacts.some((artifact) => artifact.digest === manifest.revision.artifactDigest)) {
|
|
223
|
+
ctx.addIssue({
|
|
224
|
+
code: z.ZodIssueCode.custom,
|
|
225
|
+
path: ['revision', 'artifactDigest'],
|
|
226
|
+
message: 'the served artifact digest must appear among the signed artifacts',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const paths = manifest.artifacts.map((artifact) => artifact.path);
|
|
230
|
+
if (new Set(paths).size !== paths.length) {
|
|
231
|
+
ctx.addIssue({
|
|
232
|
+
code: z.ZodIssueCode.custom,
|
|
233
|
+
path: ['artifacts'],
|
|
234
|
+
message: 'each artifact path appears once in a release',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Two signatures from one key are one signature written twice, and a
|
|
238
|
+
// duplicate would make a "two independent signers" check pass on one signer.
|
|
239
|
+
const keyIds = manifest.signatures.map((signature) => signature.keyId);
|
|
240
|
+
if (new Set(keyIds).size !== keyIds.length) {
|
|
241
|
+
ctx.addIssue({
|
|
242
|
+
code: z.ZodIssueCode.custom,
|
|
243
|
+
path: ['signatures'],
|
|
244
|
+
message: 'each signing key signs a manifest once',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* Decided in: docs/adr/0008-catalogue-concept-separation.md.
|
|
28
28
|
*/
|
|
29
29
|
import { z } from 'zod';
|
|
30
|
-
import { deploymentIdSchema, inferenceDateSchema, inferenceHttpsUrlSchema, inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelIdSchema, modelReferenceSchema, modelRevisionLabelSchema, modelSlugSchema, publisherSlugSchema, RESERVED_ALIA_PUBLISHER, routingProfileSlugSchema, } from './identifiers.js';
|
|
30
|
+
import { deploymentIdSchema, inferenceDateSchema, inferenceHttpsUrlSchema, inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelIdSchema, modelReferenceSchema, modelRevisionLabelSchema, modelSlugSchema, publisherSlugSchema, RESERVED_ALIA_PUBLISHER, routingProfileSlugSchema, sha256DigestSchema, } from './identifiers.js';
|
|
31
31
|
import { priceSnapshotSchema } from './priceVersion.js';
|
|
32
32
|
/* -------------------------------------------------------------------------- */
|
|
33
33
|
/* Shared catalogue vocabulary */
|
|
@@ -279,10 +279,7 @@ export const modelRevisionSchema = z
|
|
|
279
279
|
releasedAt: inferenceTimestampSchema,
|
|
280
280
|
retiredAt: inferenceTimestampSchema.optional(),
|
|
281
281
|
/** Digest of the served artifact, where Oxy hosts the weights itself. */
|
|
282
|
-
artifactDigest:
|
|
283
|
-
.string()
|
|
284
|
-
.regex(/^sha256:[a-f0-9]{64}$/, 'artifact digest must be sha256:<64 lowercase hex>')
|
|
285
|
-
.optional(),
|
|
282
|
+
artifactDigest: sha256DigestSchema.optional(),
|
|
286
283
|
modelCardUrl: inferenceHttpsUrlSchema.optional(),
|
|
287
284
|
evaluations: z.array(modelEvaluationResultSchema).default([]),
|
|
288
285
|
safety: modelSafetyMetadataSchema.optional(),
|
|
@@ -59,8 +59,16 @@ export const oxyCredentialIdSchema = z.string().min(1).max(64);
|
|
|
59
59
|
* traceable as one that was served (ADR 0007, and step 1 of ADR 0010's edge
|
|
60
60
|
* order). It is required on the inbound envelope, which is what makes the data
|
|
61
61
|
* plane a consumer of this id rather than its source: the data plane echoes it
|
|
62
|
-
* on every stream event, on the usage report
|
|
63
|
-
* about later.
|
|
62
|
+
* on every stream event, on the usage report, in its response header and on
|
|
63
|
+
* anything it can be asked about later. It never mints one for a request it
|
|
64
|
+
* received.
|
|
65
|
+
*
|
|
66
|
+
* The one case that is NOT an exception to that: an envelope the data plane
|
|
67
|
+
* cannot read or authenticate carries no id to echo, so its rejection is
|
|
68
|
+
* labelled with an id of the data plane's own — visibly local, and never
|
|
69
|
+
* correlated with an Oxy request, because there is no Oxy request it belongs to.
|
|
70
|
+
* Saying so is what stops "consumer, not source" from being read as forbidding
|
|
71
|
+
* the only id such a rejection could have.
|
|
64
72
|
*
|
|
65
73
|
* Correlates the Oxy edge, the data plane, the financial ledger and the
|
|
66
74
|
* customer-visible receipt, so it appears on every stream event and every
|
|
@@ -106,6 +114,18 @@ export const inferenceHttpsUrlSchema = z
|
|
|
106
114
|
.string()
|
|
107
115
|
.max(2048)
|
|
108
116
|
.regex(/^https:\/\/[^\s]+$/, 'must be an absolute https URL');
|
|
117
|
+
/**
|
|
118
|
+
* A content digest, `sha256:<64 lowercase hex>`.
|
|
119
|
+
*
|
|
120
|
+
* ONE spelling, because a digest is compared for equality and nothing else: an
|
|
121
|
+
* uppercase or unprefixed variant of the same hash is a different string, so two
|
|
122
|
+
* records describing the same bytes would not match. Lowercase hex with the
|
|
123
|
+
* algorithm prefix is what the `inference_model_revisions` CHECK stores and what
|
|
124
|
+
* every artifact registry emits.
|
|
125
|
+
*/
|
|
126
|
+
export const sha256DigestSchema = z
|
|
127
|
+
.string()
|
|
128
|
+
.regex(/^sha256:[a-f0-9]{64}$/, 'digest must be sha256:<64 lowercase hex>');
|
|
109
129
|
/* -------------------------------------------------------------------------- */
|
|
110
130
|
/* Catalogue references */
|
|
111
131
|
/* -------------------------------------------------------------------------- */
|
|
@@ -11,17 +11,23 @@
|
|
|
11
11
|
*
|
|
12
12
|
* What the envelope carries that a provider request does not: the resolved
|
|
13
13
|
* attribution block (who pays, which application, which credential, which
|
|
14
|
-
* delegated user), the exact routing policy reference
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* delegated user), the exact routing policy reference, the routes that policy
|
|
15
|
+
* has already authorized, and the customer's idempotency key. Those are the
|
|
16
|
+
* fields that make a request billable and explainable, and they are resolved
|
|
17
|
+
* BEFORE the request enters the data plane.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
+
* The policy VALUES never travel. `routingPolicy` is a reference — provenance
|
|
20
|
+
* for the receipt — and `authorizedRoutes` is the result of applying the policy,
|
|
21
|
+
* in preference order, so the data plane needs no policy semantics to fail over.
|
|
22
|
+
*
|
|
23
|
+
* Decided in: docs/adr/0010-public-api-compatibility.md,
|
|
24
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md.
|
|
19
25
|
*/
|
|
20
26
|
import { z } from 'zod';
|
|
21
27
|
import { inferenceAttributionSchema } from './attribution.js';
|
|
22
28
|
import { inferenceModalitySchema } from './catalogue.js';
|
|
23
29
|
import { idempotencyKeySchema, inferenceTimestampSchema } from './identifiers.js';
|
|
24
|
-
import { routingPolicyReferenceSchema, routingTargetSchema } from './routingPolicy.js';
|
|
30
|
+
import { authorizedRouteSchema, routingPolicyReferenceSchema, routingTargetSchema, } from './routingPolicy.js';
|
|
25
31
|
/* -------------------------------------------------------------------------- */
|
|
26
32
|
/* Input */
|
|
27
33
|
/* -------------------------------------------------------------------------- */
|
|
@@ -42,7 +48,38 @@ export const inferenceContentSourceSchema = z.discriminatedUnion('kind', [
|
|
|
42
48
|
})
|
|
43
49
|
.strict(),
|
|
44
50
|
]);
|
|
45
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* One part of a message's content. A message is always a list of parts.
|
|
53
|
+
*
|
|
54
|
+
* ## `refusal` is a member and `reasoning` is not, and the asymmetry is the point
|
|
55
|
+
*
|
|
56
|
+
* A model that DECLINES says why, and those words are meant for the customer:
|
|
57
|
+
* they are the difference between "rephrase this" and "stop asking". Before this
|
|
58
|
+
* member existed there was nowhere in an {@link InferenceMessage} to put them, so
|
|
59
|
+
* a non-streaming fold kept `finishReason: 'refusal'` and dropped the sentence —
|
|
60
|
+
* the customer learned they were refused and not why, while a streaming caller of
|
|
61
|
+
* the same request got the whole explanation on the `refusal` delta channel.
|
|
62
|
+
*
|
|
63
|
+
* Reasoning is the opposite case and stays absent. It is the model's private
|
|
64
|
+
* working, and a `text` part is where somebody would put it — which renders
|
|
65
|
+
* private reasoning to the customer AS the answer, the product bug the delta
|
|
66
|
+
* channels exist to prevent. An opaque per-block reasoning blob crossing this
|
|
67
|
+
* boundary needs a home nobody has chosen yet, and inventing one here would
|
|
68
|
+
* choose it by accident.
|
|
69
|
+
*
|
|
70
|
+
* Both public dialects can carry a refusal, which is what separates the two
|
|
71
|
+
* cases at the boundary as well as in principle: `refusal` is OpenAI's OWN field
|
|
72
|
+
* in both of its shapes (`delta.refusal` streaming, `message.refusal`
|
|
73
|
+
* non-streaming), while reasoning has no OpenAI field at all — the
|
|
74
|
+
* `reasoning_content`/`reasoning` spellings an OpenAI-compatible provider emits
|
|
75
|
+
* are provider extensions. So carrying the refusal costs no dialect its
|
|
76
|
+
* standard-client parseability, and carrying reasoning would.
|
|
77
|
+
*
|
|
78
|
+
* The part is a MEMBER rather than a field because a refusal need not have text:
|
|
79
|
+
* an Anthropic `stop_reason: "refusal"` maps to the finish reason and separates
|
|
80
|
+
* no words from the answer, while an OpenAI-compatible `refusal` does. A required
|
|
81
|
+
* field would force the first provider to invent a sentence.
|
|
82
|
+
*/
|
|
46
83
|
export const inferenceContentPartSchema = z.discriminatedUnion('type', [
|
|
47
84
|
z.object({ type: z.literal('text'), text: z.string() }).strict(),
|
|
48
85
|
z
|
|
@@ -61,6 +98,11 @@ export const inferenceContentPartSchema = z.discriminatedUnion('type', [
|
|
|
61
98
|
filename: z.string().max(255).optional(),
|
|
62
99
|
})
|
|
63
100
|
.strict(),
|
|
101
|
+
/**
|
|
102
|
+
* A model's explanation for declining. Its own part, never a `text` one, so no
|
|
103
|
+
* renderer can present a refusal as the answer that was asked for.
|
|
104
|
+
*/
|
|
105
|
+
z.object({ type: z.literal('refusal'), text: z.string() }).strict(),
|
|
64
106
|
]);
|
|
65
107
|
/**
|
|
66
108
|
* A tool call an assistant made, in the normalized form.
|
|
@@ -125,6 +167,20 @@ export const inferenceMessageSchema = z
|
|
|
125
167
|
message: 'only an assistant message makes tool calls',
|
|
126
168
|
});
|
|
127
169
|
}
|
|
170
|
+
// Same rule as `toolCalls`, for the same reason: only the assistant can
|
|
171
|
+
// decline, so a refusal on any other role is a part every provider would
|
|
172
|
+
// silently ignore — and one a renderer might not.
|
|
173
|
+
if (message.role !== 'assistant') {
|
|
174
|
+
for (const [index, part] of message.content.entries()) {
|
|
175
|
+
if (part.type === 'refusal') {
|
|
176
|
+
ctx.addIssue({
|
|
177
|
+
code: z.ZodIssueCode.custom,
|
|
178
|
+
path: ['content', index, 'type'],
|
|
179
|
+
message: 'only an assistant message carries a refusal',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
128
184
|
});
|
|
129
185
|
/**
|
|
130
186
|
* The request's input.
|
|
@@ -238,6 +294,17 @@ export const clientRequestMetadataSchema = z
|
|
|
238
294
|
/* -------------------------------------------------------------------------- */
|
|
239
295
|
/* The envelope */
|
|
240
296
|
/* -------------------------------------------------------------------------- */
|
|
297
|
+
/**
|
|
298
|
+
* The model LINE a reference names, with any pinned revision dropped.
|
|
299
|
+
*
|
|
300
|
+
* Substitution is a question about the line — `anthropic/claude-opus-5` — never
|
|
301
|
+
* about the revision, so the comparisons below have to be made on it. Same split
|
|
302
|
+
* `resolveEdgeRoute` makes on the way in.
|
|
303
|
+
*/
|
|
304
|
+
const modelLineOf = (reference) => {
|
|
305
|
+
const at = reference.indexOf('@');
|
|
306
|
+
return at === -1 ? reference : reference.slice(0, at);
|
|
307
|
+
};
|
|
241
308
|
/**
|
|
242
309
|
* The canonical internal request Oxy forwards to the data plane.
|
|
243
310
|
*
|
|
@@ -265,6 +332,38 @@ export const inferenceRequestSchema = z
|
|
|
265
332
|
idempotencyKey: idempotencyKeySchema.optional(),
|
|
266
333
|
/** The exact policy revision this request is served under. */
|
|
267
334
|
routingPolicy: routingPolicyReferenceSchema,
|
|
335
|
+
/**
|
|
336
|
+
* The routes the control plane has already authorized for this request, in
|
|
337
|
+
* PREFERENCE ORDER. The first entry is the primary route Oxy resolved; the
|
|
338
|
+
* data plane fails over by taking the next one.
|
|
339
|
+
*
|
|
340
|
+
* This is what closes the gap ADR 0010's amendment left open. That amendment
|
|
341
|
+
* assigns the data plane "failover within the destinations the policy
|
|
342
|
+
* authorized" — and the envelope named no destinations, so a data plane could
|
|
343
|
+
* only fail over by re-deriving the customer's policy from values it does not
|
|
344
|
+
* have. Enumerating the survivors instead means a route switch outside the
|
|
345
|
+
* policy is impossible BY CONSTRUCTION rather than by two enforcement engines
|
|
346
|
+
* agreeing in two languages.
|
|
347
|
+
*
|
|
348
|
+
* **Absent means no failover is authorized, never "choose freely."** It is
|
|
349
|
+
* the state every envelope built before this field existed is in, and the
|
|
350
|
+
* behaviour a data plane that reads no list must already have: resolve the
|
|
351
|
+
* `target` and serve it or fail. Permission is granted by an ENTRY, so its
|
|
352
|
+
* absence can only ever narrow, and there is no reading of an absent list
|
|
353
|
+
* that widens what may be served.
|
|
354
|
+
*
|
|
355
|
+
* An EMPTY list is refused rather than treated as that state. `[]` would say
|
|
356
|
+
* "no route is authorized at all", which contradicts an envelope that was
|
|
357
|
+
* built to be served, and it is exactly the "permission granted, destination
|
|
358
|
+
* unnamed" shape `authorizedRouteSchema` exists to make unrepresentable.
|
|
359
|
+
*
|
|
360
|
+
* No price rides here. Oxy sized the hold against the most expensive route
|
|
361
|
+
* the policy permits (`usageReservationRequestSchema.ceilingPriceVersionId`),
|
|
362
|
+
* and every entry is one that policy permitted, so no failover among them can
|
|
363
|
+
* exceed it. A per-route price would also be a second authority for ranking,
|
|
364
|
+
* beside the order this list already carries.
|
|
365
|
+
*/
|
|
366
|
+
authorizedRoutes: z.array(authorizedRouteSchema).min(1).optional(),
|
|
268
367
|
})
|
|
269
368
|
.superRefine((request, ctx) => {
|
|
270
369
|
if (request.toolChoice !== undefined && request.tools.length === 0) {
|
|
@@ -282,4 +381,88 @@ export const inferenceRequestSchema = z
|
|
|
282
381
|
message: 'tool names must be unique within one request',
|
|
283
382
|
});
|
|
284
383
|
}
|
|
384
|
+
const routes = request.authorizedRoutes;
|
|
385
|
+
// The emptiness is RE-CHECKED rather than assumed away by `.min(1)`. A
|
|
386
|
+
// failed `.min()` marks the parse dirty rather than aborting it, so zod runs
|
|
387
|
+
// this refinement with the empty array still in hand; `[]` is already
|
|
388
|
+
// refused above, and reading `routes[0]` here would throw instead.
|
|
389
|
+
if (routes === undefined || routes.length === 0)
|
|
390
|
+
return;
|
|
391
|
+
const primary = routes[0];
|
|
392
|
+
// The primary is not a substitution for itself, and every `substitution`
|
|
393
|
+
// value is read RELATIVE to it. A list whose first entry claims to be a
|
|
394
|
+
// cross-model substitute names no original to have substituted for.
|
|
395
|
+
if (primary.substitution !== 'same_model') {
|
|
396
|
+
ctx.addIssue({
|
|
397
|
+
code: z.ZodIssueCode.custom,
|
|
398
|
+
path: ['authorizedRoutes', 0, 'substitution'],
|
|
399
|
+
message: 'the first authorized route is the primary and cannot be a substitution',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const primaryLine = modelLineOf(primary.modelReference);
|
|
403
|
+
// A request that named a concrete model is served or refused, never
|
|
404
|
+
// substituted, and one that PINNED a revision is served on exactly those
|
|
405
|
+
// weights. Both checks are on the primary, because a primary that already
|
|
406
|
+
// drifted makes every entry after it a substitution nobody labelled.
|
|
407
|
+
if (request.target.kind === 'model') {
|
|
408
|
+
const targetReference = request.target.modelReference;
|
|
409
|
+
const targetIsPinned = targetReference.includes('@');
|
|
410
|
+
if (targetIsPinned && primary.modelReference !== targetReference) {
|
|
411
|
+
ctx.addIssue({
|
|
412
|
+
code: z.ZodIssueCode.custom,
|
|
413
|
+
path: ['authorizedRoutes', 0, 'modelReference'],
|
|
414
|
+
message: 'a pinned request is served on exactly the revision it pinned',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (!targetIsPinned && primaryLine !== targetReference) {
|
|
418
|
+
ctx.addIssue({
|
|
419
|
+
code: z.ZodIssueCode.custom,
|
|
420
|
+
path: ['authorizedRoutes', 0, 'modelReference'],
|
|
421
|
+
message: 'the primary authorized route must serve the model the request named',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (targetIsPinned) {
|
|
425
|
+
for (const [index, route] of routes.entries()) {
|
|
426
|
+
if (route.substitution === 'cross_model') {
|
|
427
|
+
ctx.addIssue({
|
|
428
|
+
code: z.ZodIssueCode.custom,
|
|
429
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
430
|
+
message: 'a request that pinned a revision authorizes no cross-model substitute',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const [index, route] of routes.entries()) {
|
|
437
|
+
const line = modelLineOf(route.modelReference);
|
|
438
|
+
// A mislabelled entry is the whole failure mode: `same_model` on a
|
|
439
|
+
// different model line is a substitution wearing the label that needs no
|
|
440
|
+
// authorization, and `cross_model` on the same line claims an
|
|
441
|
+
// authorization the customer never had to give.
|
|
442
|
+
if (route.substitution === 'same_model' && line !== primaryLine) {
|
|
443
|
+
ctx.addIssue({
|
|
444
|
+
code: z.ZodIssueCode.custom,
|
|
445
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
446
|
+
message: `route ${index} serves ${line}, not ${primaryLine}, so it is a cross-model substitute`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (route.substitution === 'cross_model' && line === primaryLine) {
|
|
450
|
+
ctx.addIssue({
|
|
451
|
+
code: z.ZodIssueCode.custom,
|
|
452
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
453
|
+
message: `route ${index} serves ${primaryLine}, so it is same-model failover`,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Failing over to the deployment that just failed is not failover. The
|
|
458
|
+
// duplicate would also make `routeSwitches` count a switch that changed
|
|
459
|
+
// nothing.
|
|
460
|
+
const deployments = routes.map((route) => route.deploymentId);
|
|
461
|
+
if (new Set(deployments).size !== deployments.length) {
|
|
462
|
+
ctx.addIssue({
|
|
463
|
+
code: z.ZodIssueCode.custom,
|
|
464
|
+
path: ['authorizedRoutes'],
|
|
465
|
+
message: 'each deployment appears at most once in the authorized route list',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
285
468
|
});
|