fix(ai,agent,coding-agent): share UUIDv7 and use for Codex (#6834)

This commit is contained in:
Alexey Zaytsev
2026-07-19 20:23:47 -03:00
committed by GitHub
parent 956074697f
commit d2f8dafb0f
11 changed files with 23 additions and 26 deletions
+5
View File
@@ -2,8 +2,13 @@
## [Unreleased]
### Added
- Added a shared `uuidv7` utility for time-ordered identifiers.
### Fixed
- Fixed sessionless OpenAI Codex WebSocket requests to use UUIDv7 request IDs, enabling models that reject UUIDv4 IDs.
- Fixed GitHub Copilot long-context pricing tiers in generated model metadata ([#6668](https://github.com/earendil-works/pi/issues/6668)).
- Fixed Kimi Coding subscription models to report API-equivalent implied costs when models.dev reports zero pricing.
- Fixed OpenAI Responses early stream endings to be classified as retryable provider errors ([#6727](https://github.com/earendil-works/pi/issues/6727)).
@@ -46,6 +46,7 @@ import { formatProviderError, normalizeProviderError } from "../utils/error-body
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts";
import { uuidv7 } from "../utils/uuid.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
@@ -259,7 +260,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
body = nextBody as RequestBody;
}
const codexSessionId = clampOpenAIPromptCacheKey(options?.sessionId);
const websocketRequestId = codexSessionId || createCodexRequestId();
const websocketRequestId = codexSessionId || uuidv7();
const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, codexSessionId);
const websocketHeaders = buildWebSocketHeaders(
model.headers,
@@ -1505,13 +1506,6 @@ function extractAccountId(token: string): string {
}
}
function createCodexRequestId(): string {
if (typeof globalThis.crypto?.randomUUID === "function") {
return globalThis.crypto.randomUUID();
}
return `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
function buildBaseCodexHeaders(
initHeaders: Record<string, string> | undefined,
additionalHeaders: ProviderHeaders | undefined,
+1
View File
@@ -42,4 +42,5 @@ export * from "./utils/json-parse.ts";
export * from "./utils/overflow.ts";
export * from "./utils/retry.ts";
export * from "./utils/typebox-helpers.ts";
export { uuidv7 } from "./utils/uuid.ts";
export * from "./utils/validation.ts";
+48
View File
@@ -0,0 +1,48 @@
let lastTimestamp = -Infinity;
let sequence = 0;
function fillRandomBytes(bytes: Uint8Array<ArrayBuffer>): void {
if (globalThis.crypto?.getRandomValues) {
globalThis.crypto.getRandomValues(bytes);
return;
}
for (let i = 0; i < bytes.length; i++) {
bytes[i] = Math.floor(Math.random() * 256);
}
}
/** Generate a time-ordered UUIDv7. */
export function uuidv7(): string {
const random = new Uint8Array(16);
fillRandomBytes(random);
const timestamp = Date.now();
if (timestamp > lastTimestamp) {
sequence = random[6] * 0x1000000 + random[7] * 0x10000 + random[8] * 0x100 + random[9];
lastTimestamp = timestamp;
} else {
sequence = (sequence + 1) >>> 0;
if (sequence === 0) lastTimestamp++;
}
const bytes = new Uint8Array(16);
bytes[0] = (lastTimestamp / 0x10000000000) & 0xff;
bytes[1] = (lastTimestamp / 0x100000000) & 0xff;
bytes[2] = (lastTimestamp / 0x1000000) & 0xff;
bytes[3] = (lastTimestamp / 0x10000) & 0xff;
bytes[4] = (lastTimestamp / 0x100) & 0xff;
bytes[5] = lastTimestamp & 0xff;
bytes[6] = 0x70 | ((sequence >>> 28) & 0x0f);
bytes[7] = (sequence >>> 20) & 0xff;
bytes[8] = 0x80 | ((sequence >>> 14) & 0x3f);
bytes[9] = (sequence >>> 6) & 0xff;
bytes[10] = ((sequence & 0x3f) << 2) | (random[10] & 0x03);
bytes[11] = random[11];
bytes[12] = random[12];
bytes[13] = random[13];
bytes[14] = random[14];
bytes[15] = random[15];
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
}
+50
View File
@@ -0,0 +1,50 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { uuidv7 } from "../src/utils/uuid.ts";
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const TIMESTAMP = 0x0123456789ab;
function parseTimestamp(uuid: string): number {
return Number.parseInt(uuid.replaceAll("-", "").slice(0, 12), 16);
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("uuidv7", () => {
it("uses the RFC 9562 layout and preserves monotonic order", () => {
const randomValues = [
new Uint8Array([0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x11, 0x22, 0x33, 0x44, 0x55]),
new Uint8Array(16),
new Uint8Array(16),
];
const getRandomValues = vi.fn((bytes: Uint8Array) => {
bytes.set(randomValues.shift() ?? new Uint8Array(bytes.length));
return bytes;
});
vi.stubGlobal("crypto", { getRandomValues });
const dateNow = vi.spyOn(Date, "now").mockReturnValue(TIMESTAMP);
try {
const first = uuidv7();
const second = uuidv7();
const third = uuidv7();
expect(first).toBe("01234567-89ab-7fff-bfff-f91122334455");
expect(second).toBe("01234567-89ab-7fff-bfff-fc0000000000");
expect(third).toBe("01234567-89ac-7000-8000-000000000000");
expect(first).toMatch(UUID_V7_RE);
expect(second).toMatch(UUID_V7_RE);
expect(third).toMatch(UUID_V7_RE);
expect(parseTimestamp(first)).toBe(TIMESTAMP);
expect(parseTimestamp(second)).toBe(TIMESTAMP);
expect(parseTimestamp(third)).toBe(TIMESTAMP + 1);
expect(first < second).toBe(true);
expect(second < third).toBe(true);
expect(getRandomValues).toHaveBeenCalledTimes(3);
} finally {
dateNow.mockRestore();
}
});
});