@peac/adapter-openai-compatible 0.12.1 → 0.12.3
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/crypto.d.ts +1 -1
- package/dist/evidence.d.ts +2 -2
- package/dist/hash.d.ts +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +3 -3
package/dist/crypto.d.ts
CHANGED
package/dist/evidence.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Evidence mapping for OpenAI-compatible chat completions
|
|
2
|
+
* Evidence mapping for OpenAI-compatible chat completions.
|
|
3
3
|
*
|
|
4
4
|
* Maps a ChatCompletion into InteractionEvidenceV01 using the hash-first
|
|
5
5
|
* model: SHA-256 digests of messages and output, plaintext metadata only.
|
|
@@ -44,7 +44,7 @@ export interface InferenceEvidence {
|
|
|
44
44
|
/**
|
|
45
45
|
* Create interaction evidence from an OpenAI-compatible chat completion.
|
|
46
46
|
*
|
|
47
|
-
* Hash-first model
|
|
47
|
+
* Hash-first model: no raw prompt or completion text is stored.
|
|
48
48
|
* Only SHA-256 digests, model ID, token counts, and timing are recorded.
|
|
49
49
|
*
|
|
50
50
|
* @param params - Messages, completion response, and optional provider
|
package/dist/hash.d.ts
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/crypto.ts","../src/hash.ts","../src/evidence.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * WebCrypto utility for SHA-256 hashing (DD-138).\n *\n * Portable: uses globalThis.crypto.subtle (Node 19+, browsers, Deno, Bun)\n * with fallback to node:crypto webcrypto for Node >=16.\n *\n * Minimum supported runtime: Node.js 16+ (with webcrypto).\n * Recommended: Node.js 22+ (globalThis.crypto.subtle is always available).\n */\n\n// Use typeof to avoid DOM lib dependency for the SubtleCrypto type\ntype Subtle = typeof globalThis.crypto.subtle;\n\n// ---------------------------------------------------------------------------\n// WebCrypto resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Get the SubtleCrypto implementation for the current runtime.\n *\n * Tries globalThis.crypto.subtle first (available in Node 19+, all modern\n * browsers, Deno, Bun, Cloudflare Workers). Falls back to node:crypto\n * webcrypto for Node >=16.\n *\n * @throws {Error} if no WebCrypto implementation is available\n */\nexport function getSubtle(): Subtle {\n // Prefer globalThis.crypto.subtle (Node 19+, all modern browsers)\n const subtle = globalThis?.crypto?.subtle;\n if (subtle) return subtle;\n\n // Node.js fallback for older versions (>=16)\n try {\n const nodeCrypto = require('node:crypto') as {\n webcrypto: { subtle: Subtle };\n };\n return nodeCrypto.webcrypto.subtle;\n } catch {\n throw new Error(\n 'No WebCrypto implementation available. ' +\n 'Requires Node.js >=16, or a runtime with globalThis.crypto.subtle.'\n );\n }\n}\n","/**\n * Hash-first evidence model for OpenAI-compatible completions (DD-138).\n *\n * SHA-256 digests of messages and output.\n * No raw prompt or completion text is stored in receipts.\n *\n * Canonicalization: deterministic key-sorted JSON serialization.\n * Object keys are sorted lexicographically at every nesting level,\n * then serialized via JSON.stringify. This is NOT RFC 8785 JCS\n * (which has additional requirements for numeric handling); it is\n * sufficient for ChatMessage objects whose fields are strings,\n * booleans, nulls, and arrays of the same.\n *\n * Input constraints enforced by canonicalize():\n * - Allowed: string, number, boolean, null, plain object, array\n * - Rejected: Date, RegExp, Map, Set, Function, Symbol, BigInt, undefined\n * (at any nesting level)\n */\n\nimport { getSubtle } from './crypto.js';\nimport type { ChatMessage } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\n/**\n * SHA-256 hash a string, return `sha256:<hex64>`.\n *\n * Uses getSubtle() for portable WebCrypto access\n * (Node 19+ globalThis.crypto.subtle, fallback to node:crypto webcrypto).\n */\nasync function sha256(input: string): Promise<string> {\n const data = encoder.encode(input);\n const subtle = getSubtle();\n const buf = await subtle.digest('SHA-256', data);\n const hex = Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return `sha256:${hex}`;\n}\n\n// ---------------------------------------------------------------------------\n// Canonicalization\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively sort object keys for deterministic serialization.\n *\n * This produces a stable JSON representation by sorting object keys\n * lexicographically at every nesting level. Arrays preserve order.\n *\n * Note: this is deterministic key-sorted JSON, not RFC 8785 JCS.\n * Suitable for ChatMessage fields (strings, nulls, arrays, booleans).\n *\n * Input constraints: only JSON-safe types are accepted (string, number,\n * boolean, null, plain object, array). Date, RegExp, Map, Set, Function,\n * Symbol, BigInt, and undefined are rejected with a TypeError.\n *\n * @throws {TypeError} if input contains non-JSON-safe types\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null) return value;\n\n if (value === undefined) {\n throw new TypeError('canonicalize: undefined is not allowed; use null instead');\n }\n\n const t = typeof value;\n\n // Primitives: string, number, boolean pass through\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return value;\n }\n\n // Reject non-JSON-safe primitives\n if (t === 'bigint') {\n throw new TypeError('canonicalize: BigInt is not JSON-serializable');\n }\n if (t === 'symbol') {\n throw new TypeError('canonicalize: Symbol is not JSON-serializable');\n }\n if (t === 'function') {\n throw new TypeError('canonicalize: Function is not JSON-serializable');\n }\n\n // Object types\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n // Reject non-plain objects (Date, RegExp, Map, Set, etc.)\n if (value instanceof Date) {\n throw new TypeError('canonicalize: Date objects are not allowed; use ISO 8601 strings');\n }\n if (value instanceof RegExp) {\n throw new TypeError('canonicalize: RegExp objects are not JSON-serializable');\n }\n if (value instanceof Map || value instanceof Set) {\n throw new TypeError(\n 'canonicalize: Map/Set are not JSON-serializable; use plain objects/arrays'\n );\n }\n\n // Plain object: sort keys recursively\n if (t === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n\n // Catch-all for any other unexpected type\n throw new TypeError(`canonicalize: unsupported type \"${t}\"`);\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Hash an array of chat messages.\n *\n * Applies deterministic key-sorted JSON serialization (sorted keys at\n * every nesting level), then computes SHA-256. Returns `sha256:<hex64>`.\n *\n * @param messages - The chat messages to hash\n * @returns `sha256:<hex64>` digest string\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport async function hashMessages(messages: ChatMessage[]): Promise<string> {\n const canonical = JSON.stringify(canonicalize(messages));\n return sha256(canonical);\n}\n\n/**\n * Compute the byte size of a messages array (canonical JSON).\n *\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport function messagesBytes(messages: ChatMessage[]): number {\n const canonical = JSON.stringify(canonicalize(messages));\n return encoder.encode(canonical).byteLength;\n}\n\n/**\n * Hash chat completion output text.\n *\n * Computes SHA-256 of the output content string.\n * Returns `sha256:<hex64>`.\n *\n * @param content - The output text to hash (concatenated choice contents)\n * @returns `sha256:<hex64>` digest string\n */\nexport async function hashOutput(content: string): Promise<string> {\n return sha256(content);\n}\n\n/**\n * Compute the byte size of output content.\n */\nexport function outputBytes(content: string): number {\n return encoder.encode(content).byteLength;\n}\n","/**\n * Evidence mapping for OpenAI-compatible chat completions (DD-138).\n *\n * Maps a ChatCompletion into InteractionEvidenceV01 using the hash-first\n * model: SHA-256 digests of messages and output, plaintext metadata only.\n */\n\nimport type { ChatCompletion, ChatMessage, InferenceReceiptParams } from './types.js';\nimport { hashMessages, hashOutput, messagesBytes, outputBytes } from './hash.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Well-known interaction kind for chat completions. */\nexport const INFERENCE_KIND = 'inference.chat_completion';\n\n/** Extension key for inference metadata. */\nexport const INFERENCE_EXTENSION_KEY = 'org.peacprotocol/inference@0.1';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Interaction evidence structure (matches InteractionEvidenceV01 from @peac/schema). */\nexport interface InferenceEvidence {\n interaction_id: string;\n kind: typeof INFERENCE_KIND;\n executor: {\n platform: string;\n version?: string;\n };\n started_at: string;\n completed_at?: string;\n duration_ms?: number;\n input: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n output: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n result: {\n status: 'ok' | 'error';\n };\n extensions: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Concatenate all choice message contents into a single string. */\nfunction concatOutputContent(completion: ChatCompletion): string {\n return completion.choices.map((c) => c.message.content ?? '').join('');\n}\n\n/** Extract finish_reason from the first choice (if any). */\nfunction primaryFinishReason(completion: ChatCompletion): string | null {\n if (completion.choices.length === 0) return null;\n return completion.choices[0].finish_reason;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create interaction evidence from an OpenAI-compatible chat completion.\n *\n * Hash-first model (DD-138): no raw prompt or completion text is stored.\n * Only SHA-256 digests, model ID, token counts, and timing are recorded.\n *\n * @param params - Messages, completion response, and optional provider\n * @returns InteractionEvidenceV01-compatible object\n */\nexport async function fromChatCompletion(\n params: InferenceReceiptParams\n): Promise<InferenceEvidence> {\n const { messages, completion, provider } = params;\n\n // Hash inputs and outputs\n const outputContent = concatOutputContent(completion);\n const [inputHash, outputHash] = await Promise.all([\n hashMessages(messages),\n hashOutput(outputContent),\n ]);\n\n // Extract the hex value (strip `sha256:` prefix for digest.value)\n const inputValue = inputHash.slice('sha256:'.length);\n const outputValue = outputHash.slice('sha256:'.length);\n\n // Build platform identifier\n const platform = provider ? `openai-compatible:${provider}` : 'openai-compatible';\n\n // Build inference extension metadata\n const inferenceExt: Record<string, unknown> = {\n model: completion.model,\n finish_reason: primaryFinishReason(completion),\n };\n if (completion.usage) {\n inferenceExt.usage = {\n prompt_tokens: completion.usage.prompt_tokens,\n completion_tokens: completion.usage.completion_tokens,\n total_tokens: completion.usage.total_tokens,\n };\n }\n\n // Determine result status from finish_reason\n const finishReason = primaryFinishReason(completion);\n const resultStatus: 'ok' | 'error' =\n finishReason === 'error' || completion.choices.length === 0 ? 'error' : 'ok';\n\n const evidence: InferenceEvidence = {\n interaction_id: completion.id,\n kind: INFERENCE_KIND,\n executor: {\n platform,\n version: completion.model,\n },\n started_at: new Date(completion.created * 1000).toISOString(),\n input: {\n digest: {\n alg: 'sha-256',\n value: inputValue,\n bytes: messagesBytes(messages),\n },\n redaction: 'hash_only',\n },\n output: {\n digest: {\n alg: 'sha-256',\n value: outputValue,\n bytes: outputBytes(outputContent),\n },\n redaction: 'hash_only',\n },\n result: {\n status: resultStatus,\n },\n extensions: {\n [INFERENCE_EXTENSION_KEY]: inferenceExt,\n },\n };\n\n return evidence;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/crypto.ts","../src/hash.ts","../src/evidence.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * WebCrypto utility for SHA-256 hashing.\n *\n * Portable: uses globalThis.crypto.subtle (Node 19+, browsers, Deno, Bun)\n * with fallback to node:crypto webcrypto for Node >=16.\n *\n * Minimum supported runtime: Node.js 16+ (with webcrypto).\n * Recommended: Node.js 22+ (globalThis.crypto.subtle is always available).\n */\n\n// Use typeof to avoid DOM lib dependency for the SubtleCrypto type\ntype Subtle = typeof globalThis.crypto.subtle;\n\n// ---------------------------------------------------------------------------\n// WebCrypto resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Get the SubtleCrypto implementation for the current runtime.\n *\n * Tries globalThis.crypto.subtle first (available in Node 19+, all modern\n * browsers, Deno, Bun, Cloudflare Workers). Falls back to node:crypto\n * webcrypto for Node >=16.\n *\n * @throws {Error} if no WebCrypto implementation is available\n */\nexport function getSubtle(): Subtle {\n // Prefer globalThis.crypto.subtle (Node 19+, all modern browsers)\n const subtle = globalThis?.crypto?.subtle;\n if (subtle) return subtle;\n\n // Node.js fallback for older versions (>=16)\n try {\n const nodeCrypto = require('node:crypto') as {\n webcrypto: { subtle: Subtle };\n };\n return nodeCrypto.webcrypto.subtle;\n } catch {\n throw new Error(\n 'No WebCrypto implementation available. ' +\n 'Requires Node.js >=16, or a runtime with globalThis.crypto.subtle.'\n );\n }\n}\n","/**\n * Hash-first evidence model for OpenAI-compatible completions.\n *\n * SHA-256 digests of messages and output.\n * No raw prompt or completion text is stored in receipts.\n *\n * Canonicalization: deterministic key-sorted JSON serialization.\n * Object keys are sorted lexicographically at every nesting level,\n * then serialized via JSON.stringify. This is NOT RFC 8785 JCS\n * (which has additional requirements for numeric handling); it is\n * sufficient for ChatMessage objects whose fields are strings,\n * booleans, nulls, and arrays of the same.\n *\n * Input constraints enforced by canonicalize():\n * - Allowed: string, number, boolean, null, plain object, array\n * - Rejected: Date, RegExp, Map, Set, Function, Symbol, BigInt, undefined\n * (at any nesting level)\n */\n\nimport { getSubtle } from './crypto.js';\nimport type { ChatMessage } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\n/**\n * SHA-256 hash a string, return `sha256:<hex64>`.\n *\n * Uses getSubtle() for portable WebCrypto access\n * (Node 19+ globalThis.crypto.subtle, fallback to node:crypto webcrypto).\n */\nasync function sha256(input: string): Promise<string> {\n const data = encoder.encode(input);\n const subtle = getSubtle();\n const buf = await subtle.digest('SHA-256', data);\n const hex = Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return `sha256:${hex}`;\n}\n\n// ---------------------------------------------------------------------------\n// Canonicalization\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively sort object keys for deterministic serialization.\n *\n * This produces a stable JSON representation by sorting object keys\n * lexicographically at every nesting level. Arrays preserve order.\n *\n * Note: this is deterministic key-sorted JSON, not RFC 8785 JCS.\n * Suitable for ChatMessage fields (strings, nulls, arrays, booleans).\n *\n * Input constraints: only JSON-safe types are accepted (string, number,\n * boolean, null, plain object, array). Date, RegExp, Map, Set, Function,\n * Symbol, BigInt, and undefined are rejected with a TypeError.\n *\n * @throws {TypeError} if input contains non-JSON-safe types\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null) return value;\n\n if (value === undefined) {\n throw new TypeError('canonicalize: undefined is not allowed; use null instead');\n }\n\n const t = typeof value;\n\n // Primitives: string, number, boolean pass through\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return value;\n }\n\n // Reject non-JSON-safe primitives\n if (t === 'bigint') {\n throw new TypeError('canonicalize: BigInt is not JSON-serializable');\n }\n if (t === 'symbol') {\n throw new TypeError('canonicalize: Symbol is not JSON-serializable');\n }\n if (t === 'function') {\n throw new TypeError('canonicalize: Function is not JSON-serializable');\n }\n\n // Object types\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n // Reject non-plain objects (Date, RegExp, Map, Set, etc.)\n if (value instanceof Date) {\n throw new TypeError('canonicalize: Date objects are not allowed; use ISO 8601 strings');\n }\n if (value instanceof RegExp) {\n throw new TypeError('canonicalize: RegExp objects are not JSON-serializable');\n }\n if (value instanceof Map || value instanceof Set) {\n throw new TypeError(\n 'canonicalize: Map/Set are not JSON-serializable; use plain objects/arrays'\n );\n }\n\n // Plain object: sort keys recursively\n if (t === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n\n // Catch-all for any other unexpected type\n throw new TypeError(`canonicalize: unsupported type \"${t}\"`);\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Hash an array of chat messages.\n *\n * Applies deterministic key-sorted JSON serialization (sorted keys at\n * every nesting level), then computes SHA-256. Returns `sha256:<hex64>`.\n *\n * @param messages - The chat messages to hash\n * @returns `sha256:<hex64>` digest string\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport async function hashMessages(messages: ChatMessage[]): Promise<string> {\n const canonical = JSON.stringify(canonicalize(messages));\n return sha256(canonical);\n}\n\n/**\n * Compute the byte size of a messages array (canonical JSON).\n *\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport function messagesBytes(messages: ChatMessage[]): number {\n const canonical = JSON.stringify(canonicalize(messages));\n return encoder.encode(canonical).byteLength;\n}\n\n/**\n * Hash chat completion output text.\n *\n * Computes SHA-256 of the output content string.\n * Returns `sha256:<hex64>`.\n *\n * @param content - The output text to hash (concatenated choice contents)\n * @returns `sha256:<hex64>` digest string\n */\nexport async function hashOutput(content: string): Promise<string> {\n return sha256(content);\n}\n\n/**\n * Compute the byte size of output content.\n */\nexport function outputBytes(content: string): number {\n return encoder.encode(content).byteLength;\n}\n","/**\n * Evidence mapping for OpenAI-compatible chat completions.\n *\n * Maps a ChatCompletion into InteractionEvidenceV01 using the hash-first\n * model: SHA-256 digests of messages and output, plaintext metadata only.\n */\n\nimport type { ChatCompletion, ChatMessage, InferenceReceiptParams } from './types.js';\nimport { hashMessages, hashOutput, messagesBytes, outputBytes } from './hash.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Well-known interaction kind for chat completions. */\nexport const INFERENCE_KIND = 'inference.chat_completion';\n\n/** Extension key for inference metadata. */\nexport const INFERENCE_EXTENSION_KEY = 'org.peacprotocol/inference@0.1';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Interaction evidence structure (matches InteractionEvidenceV01 from @peac/schema). */\nexport interface InferenceEvidence {\n interaction_id: string;\n kind: typeof INFERENCE_KIND;\n executor: {\n platform: string;\n version?: string;\n };\n started_at: string;\n completed_at?: string;\n duration_ms?: number;\n input: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n output: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n result: {\n status: 'ok' | 'error';\n };\n extensions: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Concatenate all choice message contents into a single string. */\nfunction concatOutputContent(completion: ChatCompletion): string {\n return completion.choices.map((c) => c.message.content ?? '').join('');\n}\n\n/** Extract finish_reason from the first choice (if any). */\nfunction primaryFinishReason(completion: ChatCompletion): string | null {\n if (completion.choices.length === 0) return null;\n return completion.choices[0].finish_reason;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create interaction evidence from an OpenAI-compatible chat completion.\n *\n * Hash-first model: no raw prompt or completion text is stored.\n * Only SHA-256 digests, model ID, token counts, and timing are recorded.\n *\n * @param params - Messages, completion response, and optional provider\n * @returns InteractionEvidenceV01-compatible object\n */\nexport async function fromChatCompletion(\n params: InferenceReceiptParams\n): Promise<InferenceEvidence> {\n const { messages, completion, provider } = params;\n\n // Hash inputs and outputs\n const outputContent = concatOutputContent(completion);\n const [inputHash, outputHash] = await Promise.all([\n hashMessages(messages),\n hashOutput(outputContent),\n ]);\n\n // Extract the hex value (strip `sha256:` prefix for digest.value)\n const inputValue = inputHash.slice('sha256:'.length);\n const outputValue = outputHash.slice('sha256:'.length);\n\n // Build platform identifier\n const platform = provider ? `openai-compatible:${provider}` : 'openai-compatible';\n\n // Build inference extension metadata\n const inferenceExt: Record<string, unknown> = {\n model: completion.model,\n finish_reason: primaryFinishReason(completion),\n };\n if (completion.usage) {\n inferenceExt.usage = {\n prompt_tokens: completion.usage.prompt_tokens,\n completion_tokens: completion.usage.completion_tokens,\n total_tokens: completion.usage.total_tokens,\n };\n }\n\n // Determine result status from finish_reason\n const finishReason = primaryFinishReason(completion);\n const resultStatus: 'ok' | 'error' =\n finishReason === 'error' || completion.choices.length === 0 ? 'error' : 'ok';\n\n const evidence: InferenceEvidence = {\n interaction_id: completion.id,\n kind: INFERENCE_KIND,\n executor: {\n platform,\n version: completion.model,\n },\n started_at: new Date(completion.created * 1000).toISOString(),\n input: {\n digest: {\n alg: 'sha-256',\n value: inputValue,\n bytes: messagesBytes(messages),\n },\n redaction: 'hash_only',\n },\n output: {\n digest: {\n alg: 'sha-256',\n value: outputValue,\n bytes: outputBytes(outputContent),\n },\n redaction: 'hash_only',\n },\n result: {\n status: resultStatus,\n },\n extensions: {\n [INFERENCE_EXTENSION_KEY]: inferenceExt,\n },\n };\n\n return evidence;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @peac/adapter-openai-compatible
|
|
3
3
|
*
|
|
4
4
|
* OpenAI-compatible chat completion adapter for PEAC interaction evidence.
|
|
5
|
-
* Hash-first model
|
|
5
|
+
* Hash-first model: no raw prompt or completion text in receipts.
|
|
6
6
|
*
|
|
7
7
|
* Works with any OpenAI-compatible provider (OpenAI, Anthropic via adapter,
|
|
8
8
|
* Ollama, vLLM, Together, etc.) without importing their SDKs.
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/crypto.ts","../src/hash.ts","../src/evidence.ts"],"names":[],"mappings":";;;;;;;;AA0BO,SAAS,SAAA,GAAoB;AAElC,EAAA,MAAM,MAAA,GAAS,YAAY,MAAA,EAAQ,MAAA;AACnC,EAAA,IAAI,QAAQ,OAAO,MAAA;AAGnB,EAAA,IAAI;AACF,IAAA,MAAM,UAAA,GAAa,UAAQ,QAAa,CAAA;AAGxC,IAAA,OAAO,WAAW,SAAA,CAAU,MAAA;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACF;;;ACjBA,IAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAQhC,eAAe,OAAO,KAAA,EAAgC;AACpD,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,KAAK,CAAA;AACjC,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AAC/C,EAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAI,WAAW,GAAG,CAAC,EACvC,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAC,CAAA,CAC1C,IAAA,CAAK,EAAE,CAAA;AACV,EAAA,OAAO,UAAU,GAAG,CAAA,CAAA;AACtB;AAqBA,SAAS,aAAa,KAAA,EAAyB;AAC7C,EAAA,IAAI,KAAA,KAAU,MAAM,OAAO,KAAA;AAE3B,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,MAAM,IAAI,UAAU,0DAA0D,CAAA;AAAA,EAChF;AAEA,EAAA,MAAM,IAAI,OAAO,KAAA;AAGjB,EAAA,IAAI,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,QAAA,IAAY,MAAM,SAAA,EAAW;AACvD,IAAA,OAAO,KAAA;AAAA,EACT;AAGA,EAAA,IAAI,MAAM,QAAA,EAAU;AAClB,IAAA,MAAM,IAAI,UAAU,+CAA+C,CAAA;AAAA,EACrE;AACA,EAAA,IAAI,MAAM,QAAA,EAAU;AAClB,IAAA,MAAM,IAAI,UAAU,+CAA+C,CAAA;AAAA,EACrE;AACA,EAAA,IAAI,MAAM,UAAA,EAAY;AACpB,IAAA,MAAM,IAAI,UAAU,iDAAiD,CAAA;AAAA,EACvE;AAGA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,KAAA,CAAM,IAAI,YAAY,CAAA;AAAA,EAC/B;AAGA,EAAA,IAAI,iBAAiB,IAAA,EAAM;AACzB,IAAA,MAAM,IAAI,UAAU,kEAAkE,CAAA;AAAA,EACxF;AACA,EAAA,IAAI,iBAAiB,MAAA,EAAQ;AAC3B,IAAA,MAAM,IAAI,UAAU,wDAAwD,CAAA;AAAA,EAC9E;AACA,EAAA,IAAI,KAAA,YAAiB,GAAA,IAAO,KAAA,YAAiB,GAAA,EAAK;AAChD,IAAA,MAAM,IAAI,SAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAGA,EAAA,IAAI,MAAM,QAAA,EAAU;AAClB,IAAA,MAAM,SAAkC,EAAC;AACzC,IAAA,KAAA,MAAW,OAAO,MAAA,CAAO,IAAA,CAAK,KAAgC,CAAA,CAAE,MAAK,EAAG;AACtE,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,YAAA,CAAc,KAAA,CAAkC,GAAG,CAAC,CAAA;AAAA,IACpE;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAGA,EAAA,MAAM,IAAI,SAAA,CAAU,CAAA,gCAAA,EAAmC,CAAC,CAAA,CAAA,CAAG,CAAA;AAC7D;AAgBA,eAAsB,aAAa,QAAA,EAA0C;AAC3E,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,SAAA,CAAU,YAAA,CAAa,QAAQ,CAAC,CAAA;AACvD,EAAA,OAAO,OAAO,SAAS,CAAA;AACzB;AAOO,SAAS,cAAc,QAAA,EAAiC;AAC7D,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,SAAA,CAAU,YAAA,CAAa,QAAQ,CAAC,CAAA;AACvD,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,SAAS,CAAA,CAAE,UAAA;AACnC;AAWA,eAAsB,WAAW,OAAA,EAAkC;AACjE,EAAA,OAAO,OAAO,OAAO,CAAA;AACvB;AAKO,SAAS,YAAY,OAAA,EAAyB;AACnD,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA,CAAE,UAAA;AACjC;;;ACvJO,IAAM,cAAA,GAAiB;AAGvB,IAAM,uBAAA,GAA0B;AA4CvC,SAAS,oBAAoB,UAAA,EAAoC;AAC/D,EAAA,OAAO,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,CAAQ,OAAA,IAAW,EAAE,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA;AACvE;AAGA,SAAS,oBAAoB,UAAA,EAA2C;AACtE,EAAA,IAAI,UAAA,CAAW,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAC5C,EAAA,OAAO,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAA,CAAE,aAAA;AAC/B;AAeA,eAAsB,mBACpB,MAAA,EAC4B;AAC5B,EAAA,MAAM,EAAE,QAAA,EAAU,UAAA,EAAY,QAAA,EAAS,GAAI,MAAA;AAG3C,EAAA,MAAM,aAAA,GAAgB,oBAAoB,UAAU,CAAA;AACpD,EAAA,MAAM,CAAC,SAAA,EAAW,UAAU,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IAChD,aAAa,QAAQ,CAAA;AAAA,IACrB,WAAW,aAAa;AAAA,GACzB,CAAA;AAGD,EAAA,MAAM,UAAA,GAAa,SAAA,CAAU,KAAA,CAAM,SAAA,CAAU,MAAM,CAAA;AACnD,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,KAAA,CAAM,SAAA,CAAU,MAAM,CAAA;AAGrD,EAAA,MAAM,QAAA,GAAW,QAAA,GAAW,CAAA,kBAAA,EAAqB,QAAQ,CAAA,CAAA,GAAK,mBAAA;AAG9D,EAAA,MAAM,YAAA,GAAwC;AAAA,IAC5C,OAAO,UAAA,CAAW,KAAA;AAAA,IAClB,aAAA,EAAe,oBAAoB,UAAU;AAAA,GAC/C;AACA,EAAA,IAAI,WAAW,KAAA,EAAO;AACpB,IAAA,YAAA,CAAa,KAAA,GAAQ;AAAA,MACnB,aAAA,EAAe,WAAW,KAAA,CAAM,aAAA;AAAA,MAChC,iBAAA,EAAmB,WAAW,KAAA,CAAM,iBAAA;AAAA,MACpC,YAAA,EAAc,WAAW,KAAA,CAAM;AAAA,KACjC;AAAA,EACF;AAGA,EAAA,MAAM,YAAA,GAAe,oBAAoB,UAAU,CAAA;AACnD,EAAA,MAAM,eACJ,YAAA,KAAiB,OAAA,IAAW,WAAW,OAAA,CAAQ,MAAA,KAAW,IAAI,OAAA,GAAU,IAAA;AAE1E,EAAA,MAAM,QAAA,GAA8B;AAAA,IAClC,gBAAgB,UAAA,CAAW,EAAA;AAAA,IAC3B,IAAA,EAAM,cAAA;AAAA,IACN,QAAA,EAAU;AAAA,MACR,QAAA;AAAA,MACA,SAAS,UAAA,CAAW;AAAA,KACtB;AAAA,IACA,YAAY,IAAI,IAAA,CAAK,WAAW,OAAA,GAAU,GAAI,EAAE,WAAA,EAAY;AAAA,IAC5D,KAAA,EAAO;AAAA,MACL,MAAA,EAAQ;AAAA,QACN,GAAA,EAAK,SAAA;AAAA,QACL,KAAA,EAAO,UAAA;AAAA,QACP,KAAA,EAAO,cAAc,QAAQ;AAAA,OAC/B;AAAA,MACA,SAAA,EAAW;AAAA,KACb;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,MAAA,EAAQ;AAAA,QACN,GAAA,EAAK,SAAA;AAAA,QACL,KAAA,EAAO,WAAA;AAAA,QACP,KAAA,EAAO,YAAY,aAAa;AAAA,OAClC;AAAA,MACA,SAAA,EAAW;AAAA,KACb;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,MAAA,EAAQ;AAAA,KACV;AAAA,IACA,UAAA,EAAY;AAAA,MACV,CAAC,uBAAuB,GAAG;AAAA;AAC7B,GACF;AAEA,EAAA,OAAO,QAAA;AACT","file":"index.mjs","sourcesContent":["/**\n * WebCrypto utility for SHA-256 hashing (DD-138).\n *\n * Portable: uses globalThis.crypto.subtle (Node 19+, browsers, Deno, Bun)\n * with fallback to node:crypto webcrypto for Node >=16.\n *\n * Minimum supported runtime: Node.js 16+ (with webcrypto).\n * Recommended: Node.js 22+ (globalThis.crypto.subtle is always available).\n */\n\n// Use typeof to avoid DOM lib dependency for the SubtleCrypto type\ntype Subtle = typeof globalThis.crypto.subtle;\n\n// ---------------------------------------------------------------------------\n// WebCrypto resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Get the SubtleCrypto implementation for the current runtime.\n *\n * Tries globalThis.crypto.subtle first (available in Node 19+, all modern\n * browsers, Deno, Bun, Cloudflare Workers). Falls back to node:crypto\n * webcrypto for Node >=16.\n *\n * @throws {Error} if no WebCrypto implementation is available\n */\nexport function getSubtle(): Subtle {\n // Prefer globalThis.crypto.subtle (Node 19+, all modern browsers)\n const subtle = globalThis?.crypto?.subtle;\n if (subtle) return subtle;\n\n // Node.js fallback for older versions (>=16)\n try {\n const nodeCrypto = require('node:crypto') as {\n webcrypto: { subtle: Subtle };\n };\n return nodeCrypto.webcrypto.subtle;\n } catch {\n throw new Error(\n 'No WebCrypto implementation available. ' +\n 'Requires Node.js >=16, or a runtime with globalThis.crypto.subtle.'\n );\n }\n}\n","/**\n * Hash-first evidence model for OpenAI-compatible completions (DD-138).\n *\n * SHA-256 digests of messages and output.\n * No raw prompt or completion text is stored in receipts.\n *\n * Canonicalization: deterministic key-sorted JSON serialization.\n * Object keys are sorted lexicographically at every nesting level,\n * then serialized via JSON.stringify. This is NOT RFC 8785 JCS\n * (which has additional requirements for numeric handling); it is\n * sufficient for ChatMessage objects whose fields are strings,\n * booleans, nulls, and arrays of the same.\n *\n * Input constraints enforced by canonicalize():\n * - Allowed: string, number, boolean, null, plain object, array\n * - Rejected: Date, RegExp, Map, Set, Function, Symbol, BigInt, undefined\n * (at any nesting level)\n */\n\nimport { getSubtle } from './crypto.js';\nimport type { ChatMessage } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\n/**\n * SHA-256 hash a string, return `sha256:<hex64>`.\n *\n * Uses getSubtle() for portable WebCrypto access\n * (Node 19+ globalThis.crypto.subtle, fallback to node:crypto webcrypto).\n */\nasync function sha256(input: string): Promise<string> {\n const data = encoder.encode(input);\n const subtle = getSubtle();\n const buf = await subtle.digest('SHA-256', data);\n const hex = Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return `sha256:${hex}`;\n}\n\n// ---------------------------------------------------------------------------\n// Canonicalization\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively sort object keys for deterministic serialization.\n *\n * This produces a stable JSON representation by sorting object keys\n * lexicographically at every nesting level. Arrays preserve order.\n *\n * Note: this is deterministic key-sorted JSON, not RFC 8785 JCS.\n * Suitable for ChatMessage fields (strings, nulls, arrays, booleans).\n *\n * Input constraints: only JSON-safe types are accepted (string, number,\n * boolean, null, plain object, array). Date, RegExp, Map, Set, Function,\n * Symbol, BigInt, and undefined are rejected with a TypeError.\n *\n * @throws {TypeError} if input contains non-JSON-safe types\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null) return value;\n\n if (value === undefined) {\n throw new TypeError('canonicalize: undefined is not allowed; use null instead');\n }\n\n const t = typeof value;\n\n // Primitives: string, number, boolean pass through\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return value;\n }\n\n // Reject non-JSON-safe primitives\n if (t === 'bigint') {\n throw new TypeError('canonicalize: BigInt is not JSON-serializable');\n }\n if (t === 'symbol') {\n throw new TypeError('canonicalize: Symbol is not JSON-serializable');\n }\n if (t === 'function') {\n throw new TypeError('canonicalize: Function is not JSON-serializable');\n }\n\n // Object types\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n // Reject non-plain objects (Date, RegExp, Map, Set, etc.)\n if (value instanceof Date) {\n throw new TypeError('canonicalize: Date objects are not allowed; use ISO 8601 strings');\n }\n if (value instanceof RegExp) {\n throw new TypeError('canonicalize: RegExp objects are not JSON-serializable');\n }\n if (value instanceof Map || value instanceof Set) {\n throw new TypeError(\n 'canonicalize: Map/Set are not JSON-serializable; use plain objects/arrays'\n );\n }\n\n // Plain object: sort keys recursively\n if (t === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n\n // Catch-all for any other unexpected type\n throw new TypeError(`canonicalize: unsupported type \"${t}\"`);\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Hash an array of chat messages.\n *\n * Applies deterministic key-sorted JSON serialization (sorted keys at\n * every nesting level), then computes SHA-256. Returns `sha256:<hex64>`.\n *\n * @param messages - The chat messages to hash\n * @returns `sha256:<hex64>` digest string\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport async function hashMessages(messages: ChatMessage[]): Promise<string> {\n const canonical = JSON.stringify(canonicalize(messages));\n return sha256(canonical);\n}\n\n/**\n * Compute the byte size of a messages array (canonical JSON).\n *\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport function messagesBytes(messages: ChatMessage[]): number {\n const canonical = JSON.stringify(canonicalize(messages));\n return encoder.encode(canonical).byteLength;\n}\n\n/**\n * Hash chat completion output text.\n *\n * Computes SHA-256 of the output content string.\n * Returns `sha256:<hex64>`.\n *\n * @param content - The output text to hash (concatenated choice contents)\n * @returns `sha256:<hex64>` digest string\n */\nexport async function hashOutput(content: string): Promise<string> {\n return sha256(content);\n}\n\n/**\n * Compute the byte size of output content.\n */\nexport function outputBytes(content: string): number {\n return encoder.encode(content).byteLength;\n}\n","/**\n * Evidence mapping for OpenAI-compatible chat completions (DD-138).\n *\n * Maps a ChatCompletion into InteractionEvidenceV01 using the hash-first\n * model: SHA-256 digests of messages and output, plaintext metadata only.\n */\n\nimport type { ChatCompletion, ChatMessage, InferenceReceiptParams } from './types.js';\nimport { hashMessages, hashOutput, messagesBytes, outputBytes } from './hash.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Well-known interaction kind for chat completions. */\nexport const INFERENCE_KIND = 'inference.chat_completion';\n\n/** Extension key for inference metadata. */\nexport const INFERENCE_EXTENSION_KEY = 'org.peacprotocol/inference@0.1';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Interaction evidence structure (matches InteractionEvidenceV01 from @peac/schema). */\nexport interface InferenceEvidence {\n interaction_id: string;\n kind: typeof INFERENCE_KIND;\n executor: {\n platform: string;\n version?: string;\n };\n started_at: string;\n completed_at?: string;\n duration_ms?: number;\n input: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n output: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n result: {\n status: 'ok' | 'error';\n };\n extensions: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Concatenate all choice message contents into a single string. */\nfunction concatOutputContent(completion: ChatCompletion): string {\n return completion.choices.map((c) => c.message.content ?? '').join('');\n}\n\n/** Extract finish_reason from the first choice (if any). */\nfunction primaryFinishReason(completion: ChatCompletion): string | null {\n if (completion.choices.length === 0) return null;\n return completion.choices[0].finish_reason;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create interaction evidence from an OpenAI-compatible chat completion.\n *\n * Hash-first model (DD-138): no raw prompt or completion text is stored.\n * Only SHA-256 digests, model ID, token counts, and timing are recorded.\n *\n * @param params - Messages, completion response, and optional provider\n * @returns InteractionEvidenceV01-compatible object\n */\nexport async function fromChatCompletion(\n params: InferenceReceiptParams\n): Promise<InferenceEvidence> {\n const { messages, completion, provider } = params;\n\n // Hash inputs and outputs\n const outputContent = concatOutputContent(completion);\n const [inputHash, outputHash] = await Promise.all([\n hashMessages(messages),\n hashOutput(outputContent),\n ]);\n\n // Extract the hex value (strip `sha256:` prefix for digest.value)\n const inputValue = inputHash.slice('sha256:'.length);\n const outputValue = outputHash.slice('sha256:'.length);\n\n // Build platform identifier\n const platform = provider ? `openai-compatible:${provider}` : 'openai-compatible';\n\n // Build inference extension metadata\n const inferenceExt: Record<string, unknown> = {\n model: completion.model,\n finish_reason: primaryFinishReason(completion),\n };\n if (completion.usage) {\n inferenceExt.usage = {\n prompt_tokens: completion.usage.prompt_tokens,\n completion_tokens: completion.usage.completion_tokens,\n total_tokens: completion.usage.total_tokens,\n };\n }\n\n // Determine result status from finish_reason\n const finishReason = primaryFinishReason(completion);\n const resultStatus: 'ok' | 'error' =\n finishReason === 'error' || completion.choices.length === 0 ? 'error' : 'ok';\n\n const evidence: InferenceEvidence = {\n interaction_id: completion.id,\n kind: INFERENCE_KIND,\n executor: {\n platform,\n version: completion.model,\n },\n started_at: new Date(completion.created * 1000).toISOString(),\n input: {\n digest: {\n alg: 'sha-256',\n value: inputValue,\n bytes: messagesBytes(messages),\n },\n redaction: 'hash_only',\n },\n output: {\n digest: {\n alg: 'sha-256',\n value: outputValue,\n bytes: outputBytes(outputContent),\n },\n redaction: 'hash_only',\n },\n result: {\n status: resultStatus,\n },\n extensions: {\n [INFERENCE_EXTENSION_KEY]: inferenceExt,\n },\n };\n\n return evidence;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/crypto.ts","../src/hash.ts","../src/evidence.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * WebCrypto utility for SHA-256 hashing.\n *\n * Portable: uses globalThis.crypto.subtle (Node 19+, browsers, Deno, Bun)\n * with fallback to node:crypto webcrypto for Node >=16.\n *\n * Minimum supported runtime: Node.js 16+ (with webcrypto).\n * Recommended: Node.js 22+ (globalThis.crypto.subtle is always available).\n */\n\n// Use typeof to avoid DOM lib dependency for the SubtleCrypto type\ntype Subtle = typeof globalThis.crypto.subtle;\n\n// ---------------------------------------------------------------------------\n// WebCrypto resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Get the SubtleCrypto implementation for the current runtime.\n *\n * Tries globalThis.crypto.subtle first (available in Node 19+, all modern\n * browsers, Deno, Bun, Cloudflare Workers). Falls back to node:crypto\n * webcrypto for Node >=16.\n *\n * @throws {Error} if no WebCrypto implementation is available\n */\nexport function getSubtle(): Subtle {\n // Prefer globalThis.crypto.subtle (Node 19+, all modern browsers)\n const subtle = globalThis?.crypto?.subtle;\n if (subtle) return subtle;\n\n // Node.js fallback for older versions (>=16)\n try {\n const nodeCrypto = require('node:crypto') as {\n webcrypto: { subtle: Subtle };\n };\n return nodeCrypto.webcrypto.subtle;\n } catch {\n throw new Error(\n 'No WebCrypto implementation available. ' +\n 'Requires Node.js >=16, or a runtime with globalThis.crypto.subtle.'\n );\n }\n}\n","/**\n * Hash-first evidence model for OpenAI-compatible completions.\n *\n * SHA-256 digests of messages and output.\n * No raw prompt or completion text is stored in receipts.\n *\n * Canonicalization: deterministic key-sorted JSON serialization.\n * Object keys are sorted lexicographically at every nesting level,\n * then serialized via JSON.stringify. This is NOT RFC 8785 JCS\n * (which has additional requirements for numeric handling); it is\n * sufficient for ChatMessage objects whose fields are strings,\n * booleans, nulls, and arrays of the same.\n *\n * Input constraints enforced by canonicalize():\n * - Allowed: string, number, boolean, null, plain object, array\n * - Rejected: Date, RegExp, Map, Set, Function, Symbol, BigInt, undefined\n * (at any nesting level)\n */\n\nimport { getSubtle } from './crypto.js';\nimport type { ChatMessage } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\n/**\n * SHA-256 hash a string, return `sha256:<hex64>`.\n *\n * Uses getSubtle() for portable WebCrypto access\n * (Node 19+ globalThis.crypto.subtle, fallback to node:crypto webcrypto).\n */\nasync function sha256(input: string): Promise<string> {\n const data = encoder.encode(input);\n const subtle = getSubtle();\n const buf = await subtle.digest('SHA-256', data);\n const hex = Array.from(new Uint8Array(buf))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n return `sha256:${hex}`;\n}\n\n// ---------------------------------------------------------------------------\n// Canonicalization\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively sort object keys for deterministic serialization.\n *\n * This produces a stable JSON representation by sorting object keys\n * lexicographically at every nesting level. Arrays preserve order.\n *\n * Note: this is deterministic key-sorted JSON, not RFC 8785 JCS.\n * Suitable for ChatMessage fields (strings, nulls, arrays, booleans).\n *\n * Input constraints: only JSON-safe types are accepted (string, number,\n * boolean, null, plain object, array). Date, RegExp, Map, Set, Function,\n * Symbol, BigInt, and undefined are rejected with a TypeError.\n *\n * @throws {TypeError} if input contains non-JSON-safe types\n */\nfunction canonicalize(value: unknown): unknown {\n if (value === null) return value;\n\n if (value === undefined) {\n throw new TypeError('canonicalize: undefined is not allowed; use null instead');\n }\n\n const t = typeof value;\n\n // Primitives: string, number, boolean pass through\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return value;\n }\n\n // Reject non-JSON-safe primitives\n if (t === 'bigint') {\n throw new TypeError('canonicalize: BigInt is not JSON-serializable');\n }\n if (t === 'symbol') {\n throw new TypeError('canonicalize: Symbol is not JSON-serializable');\n }\n if (t === 'function') {\n throw new TypeError('canonicalize: Function is not JSON-serializable');\n }\n\n // Object types\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n\n // Reject non-plain objects (Date, RegExp, Map, Set, etc.)\n if (value instanceof Date) {\n throw new TypeError('canonicalize: Date objects are not allowed; use ISO 8601 strings');\n }\n if (value instanceof RegExp) {\n throw new TypeError('canonicalize: RegExp objects are not JSON-serializable');\n }\n if (value instanceof Map || value instanceof Set) {\n throw new TypeError(\n 'canonicalize: Map/Set are not JSON-serializable; use plain objects/arrays'\n );\n }\n\n // Plain object: sort keys recursively\n if (t === 'object') {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[key] = canonicalize((value as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n\n // Catch-all for any other unexpected type\n throw new TypeError(`canonicalize: unsupported type \"${t}\"`);\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Hash an array of chat messages.\n *\n * Applies deterministic key-sorted JSON serialization (sorted keys at\n * every nesting level), then computes SHA-256. Returns `sha256:<hex64>`.\n *\n * @param messages - The chat messages to hash\n * @returns `sha256:<hex64>` digest string\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport async function hashMessages(messages: ChatMessage[]): Promise<string> {\n const canonical = JSON.stringify(canonicalize(messages));\n return sha256(canonical);\n}\n\n/**\n * Compute the byte size of a messages array (canonical JSON).\n *\n * @throws {TypeError} if messages contain non-JSON-safe types\n */\nexport function messagesBytes(messages: ChatMessage[]): number {\n const canonical = JSON.stringify(canonicalize(messages));\n return encoder.encode(canonical).byteLength;\n}\n\n/**\n * Hash chat completion output text.\n *\n * Computes SHA-256 of the output content string.\n * Returns `sha256:<hex64>`.\n *\n * @param content - The output text to hash (concatenated choice contents)\n * @returns `sha256:<hex64>` digest string\n */\nexport async function hashOutput(content: string): Promise<string> {\n return sha256(content);\n}\n\n/**\n * Compute the byte size of output content.\n */\nexport function outputBytes(content: string): number {\n return encoder.encode(content).byteLength;\n}\n","/**\n * Evidence mapping for OpenAI-compatible chat completions.\n *\n * Maps a ChatCompletion into InteractionEvidenceV01 using the hash-first\n * model: SHA-256 digests of messages and output, plaintext metadata only.\n */\n\nimport type { ChatCompletion, ChatMessage, InferenceReceiptParams } from './types.js';\nimport { hashMessages, hashOutput, messagesBytes, outputBytes } from './hash.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Well-known interaction kind for chat completions. */\nexport const INFERENCE_KIND = 'inference.chat_completion';\n\n/** Extension key for inference metadata. */\nexport const INFERENCE_EXTENSION_KEY = 'org.peacprotocol/inference@0.1';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Interaction evidence structure (matches InteractionEvidenceV01 from @peac/schema). */\nexport interface InferenceEvidence {\n interaction_id: string;\n kind: typeof INFERENCE_KIND;\n executor: {\n platform: string;\n version?: string;\n };\n started_at: string;\n completed_at?: string;\n duration_ms?: number;\n input: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n output: {\n digest: {\n alg: 'sha-256';\n value: string;\n bytes: number;\n };\n redaction: 'hash_only';\n };\n result: {\n status: 'ok' | 'error';\n };\n extensions: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Concatenate all choice message contents into a single string. */\nfunction concatOutputContent(completion: ChatCompletion): string {\n return completion.choices.map((c) => c.message.content ?? '').join('');\n}\n\n/** Extract finish_reason from the first choice (if any). */\nfunction primaryFinishReason(completion: ChatCompletion): string | null {\n if (completion.choices.length === 0) return null;\n return completion.choices[0].finish_reason;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Create interaction evidence from an OpenAI-compatible chat completion.\n *\n * Hash-first model: no raw prompt or completion text is stored.\n * Only SHA-256 digests, model ID, token counts, and timing are recorded.\n *\n * @param params - Messages, completion response, and optional provider\n * @returns InteractionEvidenceV01-compatible object\n */\nexport async function fromChatCompletion(\n params: InferenceReceiptParams\n): Promise<InferenceEvidence> {\n const { messages, completion, provider } = params;\n\n // Hash inputs and outputs\n const outputContent = concatOutputContent(completion);\n const [inputHash, outputHash] = await Promise.all([\n hashMessages(messages),\n hashOutput(outputContent),\n ]);\n\n // Extract the hex value (strip `sha256:` prefix for digest.value)\n const inputValue = inputHash.slice('sha256:'.length);\n const outputValue = outputHash.slice('sha256:'.length);\n\n // Build platform identifier\n const platform = provider ? `openai-compatible:${provider}` : 'openai-compatible';\n\n // Build inference extension metadata\n const inferenceExt: Record<string, unknown> = {\n model: completion.model,\n finish_reason: primaryFinishReason(completion),\n };\n if (completion.usage) {\n inferenceExt.usage = {\n prompt_tokens: completion.usage.prompt_tokens,\n completion_tokens: completion.usage.completion_tokens,\n total_tokens: completion.usage.total_tokens,\n };\n }\n\n // Determine result status from finish_reason\n const finishReason = primaryFinishReason(completion);\n const resultStatus: 'ok' | 'error' =\n finishReason === 'error' || completion.choices.length === 0 ? 'error' : 'ok';\n\n const evidence: InferenceEvidence = {\n interaction_id: completion.id,\n kind: INFERENCE_KIND,\n executor: {\n platform,\n version: completion.model,\n },\n started_at: new Date(completion.created * 1000).toISOString(),\n input: {\n digest: {\n alg: 'sha-256',\n value: inputValue,\n bytes: messagesBytes(messages),\n },\n redaction: 'hash_only',\n },\n output: {\n digest: {\n alg: 'sha-256',\n value: outputValue,\n bytes: outputBytes(outputContent),\n },\n redaction: 'hash_only',\n },\n result: {\n status: resultStatus,\n },\n extensions: {\n [INFERENCE_EXTENSION_KEY]: inferenceExt,\n },\n };\n\n return evidence;\n}\n"]}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peac/adapter-openai-compatible",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "OpenAI-compatible chat completion adapter for PEAC interaction evidence (hash-first)",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@peac/kernel": "0.12.
|
|
27
|
-
"@peac/schema": "0.12.
|
|
26
|
+
"@peac/kernel": "0.12.3",
|
|
27
|
+
"@peac/schema": "0.12.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^22.19.11",
|