Files
oh-my-pi/packages/ai/src/utils.ts
T
can1357 d60ee622a4 refactor: restructured hash API and cache tracking across TypeScript and Rust
- Migrated hash API calls from Bun.hash.xxHash64() to Bun.hash() across TypeScript packages for simplified hash generation.
- Consolidated mermaid cache failure tracking by replacing separate failed Set with null values in cache Map.
- Refactored schema-based child extraction in Rust by introducing promotion_fields tracking and schema_wrapper_child() helper function.
- Updated TypeScript configuration files with reformatted arrays and added compiler options for consistency.
2026-04-10 14:26:06 +02:00

148 lines
5.1 KiB
TypeScript

import { $env } from "@oh-my-pi/pi-utils";
import type { ResponseInput } from "openai/resources/responses/responses";
import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types";
type OpenAIResponsesReplayItem = ResponseInput[number];
export { isRecord } from "@oh-my-pi/pi-utils";
export function toNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
export function toPositiveNumber(value: unknown, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return fallback;
}
return value;
}
export function toBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
export function normalizeToolCallId(id: string): string {
const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_");
return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
}
export function normalizeResponsesToolCallId(id: string): { callId: string; itemId: string } {
const [callId, itemId] = id.split("|");
if (callId && itemId) {
const normalizedCallId = truncateResponseItemId(callId, getIdPrefix(callId, "call"));
const normalizedItemId = normalizeResponsesItemId(itemId);
return { callId: normalizedCallId, itemId: normalizedItemId };
}
const hash = Bun.hash(id).toString(36);
const normalizedCallId = id.startsWith("call_") ? truncateResponseItemId(id, "call") : `call_${hash}`;
return { callId: normalizedCallId, itemId: `fc_${hash}` };
}
function getIdPrefix(id: string, fallback: string): string {
const prefix = id.match(/^([a-zA-Z][a-zA-Z0-9]*)_/)?.[1];
return prefix || fallback;
}
function normalizeResponsesItemId(itemId: string): string {
const prefix = getIdPrefix(itemId, "fc");
if (prefix !== "fc" && prefix !== "fcr") {
return `fc_${Bun.hash(itemId).toString(36)}`;
}
return truncateResponseItemId(itemId, prefix);
}
/**
* Truncate an OpenAI Responses API item ID to 64 characters.
* IDs exceeding the limit are replaced with a hash-based ID using the given prefix.
*/
export function truncateResponseItemId(id: string, prefix: string): string {
if (id.length <= 64) return id;
return `${prefix}_${Bun.hash(id).toString(36)}`;
}
export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput {
const normalizedCallIds = new Map<string, string>();
return items.flatMap(item => {
const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(item, normalizedCallIds);
return sanitized ? [sanitized] : [];
});
}
function sanitizeOpenAIResponsesHistoryItemForReplay(
item: Record<string, unknown>,
normalizedCallIds: Map<string, string>,
): OpenAIResponsesReplayItem | undefined {
if (item.type === "item_reference") return undefined;
// providerPayload stores raw output items; replay strips item ids and keeps only normalized call_id.
const { id: _id, ...sanitizedItem } = item;
if (typeof item.call_id === "string") {
sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
}
return sanitizedItem as unknown as OpenAIResponsesReplayItem;
}
function normalizeReplayedResponsesHistoryCallId(value: string, normalizedValues: Map<string, string>): string {
const normalized = normalizedValues.get(value);
if (normalized) return normalized;
const next = truncateResponseItemId(value, getIdPrefix(value, "call"));
normalizedValues.set(value, next);
return next;
}
export function createOpenAIResponsesHistoryPayload(
provider: string,
items: Array<Record<string, unknown>>,
incremental = true,
): OpenAIResponsesHistoryPayload {
return {
type: "openaiResponsesHistory",
provider,
...(incremental ? { dt: true } : {}),
items,
};
}
export function getOpenAIResponsesHistoryPayload(
providerPayload: ProviderPayload | undefined,
currentProvider: string,
fallbackProvider?: string,
): OpenAIResponsesHistoryPayload | undefined {
if (providerPayload?.type !== "openaiResponsesHistory" || !Array.isArray(providerPayload.items)) {
return undefined;
}
const payloadProvider = providerPayload.provider ?? fallbackProvider;
if (!payloadProvider || payloadProvider !== currentProvider) {
return undefined;
}
return { ...providerPayload, provider: payloadProvider };
}
export function getOpenAIResponsesHistoryItems(
providerPayload: ProviderPayload | undefined,
currentProvider: string,
fallbackProvider?: string,
): Array<Record<string, unknown>> | undefined {
return getOpenAIResponsesHistoryPayload(providerPayload, currentProvider, fallbackProvider)?.items;
}
/**
* Resolve cache retention preference.
* Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility.
*/
export function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention {
if (cacheRetention) return cacheRetention;
if ($env.PI_CACHE_RETENTION === "long") return "long";
return "short";
}
export function isAnthropicOAuthToken(key: string): boolean {
return key.includes("sk-ant-oat");
}