mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor(executors): extract pure payload construction from claude-web (#6006)
Extract the pure Claude-web payload types + transforms + default tools/style (ClaudeWebRequestPayload, ClaudeWebStreamChunk, DEFAULT_CLAUDE_MODEL, generateMessageUUIDs, getDefaultTools, getDefaultPersonalizedStyle, transformToClaude, transformFromClaude) verbatim into the leaf claude-web/payload.ts. Host imports the 3 it uses back (ClaudeWebRequestPayload type + the two transforms). Host 1056 -> 835 LOC. Byte-identical bodies (verbatim 149/149), leaf imports only randomUUID (no host import, no cycle), all module-private (no re-export). Cookie/auth/ Turnstile/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green (claude-web 13, claude-web-auto-refresh 6).
This commit is contained in:
committed by
GitHub
parent
de3fbd5b6a
commit
1a73dd2936
@@ -27,6 +27,11 @@ import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
|
||||
import { randomUUID } from "crypto";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { tryBackedChat } from "../services/browserBackedChat.ts";
|
||||
import {
|
||||
type ClaudeWebRequestPayload,
|
||||
transformToClaude,
|
||||
transformFromClaude,
|
||||
} from "./claude-web/payload.ts";
|
||||
|
||||
// ─── Constants ──────────────────────────────────────────────────────────────
|
||||
const CLAUDE_WEB_API_BASE = "https://claude.ai/api";
|
||||
@@ -87,74 +92,6 @@ function readClaudeWebDeviceId(credentials: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Default model when not specified
|
||||
const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Extended credentials to include organization and conversation context
|
||||
*/
|
||||
interface ClaudeWebRequestPayload {
|
||||
prompt: string;
|
||||
model: string;
|
||||
timezone: string;
|
||||
personalized_styles: Array<{
|
||||
type: string;
|
||||
key: string;
|
||||
name: string;
|
||||
nameKey: string;
|
||||
prompt: string;
|
||||
summary: string;
|
||||
summaryKey: string;
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
locale: string;
|
||||
tools: Array<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
input_schema?: Record<string, unknown>;
|
||||
integration_name?: string;
|
||||
is_mcp_app?: boolean;
|
||||
type?: string;
|
||||
}>;
|
||||
turn_message_uuids: {
|
||||
human_message_uuid: string;
|
||||
assistant_message_uuid: string;
|
||||
};
|
||||
attachments: unknown[];
|
||||
effort: string;
|
||||
files: unknown[];
|
||||
sync_sources: unknown[];
|
||||
rendering_mode: string;
|
||||
thinking_mode: string;
|
||||
create_conversation_params: {
|
||||
name: string;
|
||||
model: string;
|
||||
include_conversation_preferences: boolean;
|
||||
paprika_mode: unknown;
|
||||
compass_mode: unknown;
|
||||
is_temporary: boolean;
|
||||
enabled_imagine: boolean;
|
||||
tool_search_mode: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream chunk from Claude Web API
|
||||
*/
|
||||
interface ClaudeWebStreamChunk {
|
||||
type?: string;
|
||||
index?: number;
|
||||
completion?: string;
|
||||
stop_reason?: string | null;
|
||||
model?: string;
|
||||
delta?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ─── Helper Functions ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -230,166 +167,6 @@ async function normalizeClaudeSessionCookieWithAutoRefresh(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUIDs for turn message tracking
|
||||
*/
|
||||
function generateMessageUUIDs() {
|
||||
return {
|
||||
human_message_uuid: randomUUID(),
|
||||
assistant_message_uuid: randomUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default tool definitions for Claude Web API
|
||||
*/
|
||||
function getDefaultTools(): ClaudeWebRequestPayload["tools"] {
|
||||
return [
|
||||
{
|
||||
name: "show_widget",
|
||||
description: "Display interactive widgets and visualizations",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
widget_type: {
|
||||
type: "string",
|
||||
description: "Type of widget to display",
|
||||
},
|
||||
},
|
||||
},
|
||||
integration_name: "visualize",
|
||||
is_mcp_app: true,
|
||||
},
|
||||
{
|
||||
name: "read_me",
|
||||
description: "Read and reference documents",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file_path: {
|
||||
type: "string",
|
||||
description: "Path to the file to read",
|
||||
},
|
||||
},
|
||||
},
|
||||
integration_name: "visualize",
|
||||
is_mcp_app: false,
|
||||
},
|
||||
{
|
||||
type: "web_search_v0",
|
||||
name: "web_search",
|
||||
},
|
||||
{
|
||||
type: "artifacts_v0",
|
||||
name: "artifacts",
|
||||
},
|
||||
{
|
||||
type: "repl_v0",
|
||||
name: "repl",
|
||||
},
|
||||
{ type: "widget", name: "weather_fetch" },
|
||||
{ type: "widget", name: "recipe_display_v0" },
|
||||
{ type: "widget", name: "places_map_display_v0" },
|
||||
{ type: "widget", name: "message_compose_v1" },
|
||||
{ type: "widget", name: "ask_user_input_v0" },
|
||||
{ type: "widget", name: "recommend_claude_apps" },
|
||||
{ type: "widget", name: "places_search" },
|
||||
{ type: "widget", name: "fetch_sports_data" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default personalized style
|
||||
*/
|
||||
function getDefaultPersonalizedStyle(): ClaudeWebRequestPayload["personalized_styles"] {
|
||||
return [
|
||||
{
|
||||
type: "default",
|
||||
key: "Default",
|
||||
name: "Normal",
|
||||
nameKey: "normal_style_name",
|
||||
prompt: "Normal\n",
|
||||
summary: "Default responses from Claude",
|
||||
summaryKey: "normal_style_summary",
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform OpenAI format to Claude Web format
|
||||
*/
|
||||
function transformToClaude(body: Record<string, unknown>, model: string): ClaudeWebRequestPayload {
|
||||
const messages = Array.isArray(body.messages) ? body.messages : [];
|
||||
|
||||
// Extract the last user message as the prompt
|
||||
let prompt = "";
|
||||
for (const msg of messages) {
|
||||
if (typeof msg === "object" && msg !== null) {
|
||||
const message = msg as Record<string, unknown>;
|
||||
if (message.role === "user") {
|
||||
prompt = String(message.content || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prompt.trim()) {
|
||||
throw new Error("No user message found in request");
|
||||
}
|
||||
|
||||
return {
|
||||
prompt,
|
||||
model: model || DEFAULT_CLAUDE_MODEL,
|
||||
timezone: "Asia/Jakarta",
|
||||
personalized_styles: getDefaultPersonalizedStyle(),
|
||||
locale: "en-US",
|
||||
tools: getDefaultTools(),
|
||||
turn_message_uuids: generateMessageUUIDs(),
|
||||
attachments: [],
|
||||
effort: "low",
|
||||
files: [],
|
||||
sync_sources: [],
|
||||
rendering_mode: "messages",
|
||||
thinking_mode: "off",
|
||||
create_conversation_params: {
|
||||
name: "",
|
||||
model: model || DEFAULT_CLAUDE_MODEL,
|
||||
include_conversation_preferences: true,
|
||||
paprika_mode: null,
|
||||
compass_mode: null,
|
||||
is_temporary: false,
|
||||
enabled_imagine: true,
|
||||
tool_search_mode: "auto",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Claude Web response to OpenAI format
|
||||
*/
|
||||
function transformFromClaude(
|
||||
claudeContent: string,
|
||||
model: string,
|
||||
stopReason?: string
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: claudeContent,
|
||||
},
|
||||
finish_reason: stopReason === "end_turn" ? "stop" : null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify session is still valid by checking if the organizations endpoint
|
||||
* returns a successful response. Claude's API does not have a /api/auth/session
|
||||
|
||||
230
open-sse/executors/claude-web/payload.ts
Normal file
230
open-sse/executors/claude-web/payload.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// Pure Claude-web payload construction (types + transforms + default tools/style).
|
||||
// Extracted verbatim from claude-web.ts. No host state, no fetch/auth.
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
// Default model when not specified
|
||||
export const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
|
||||
|
||||
export interface ClaudeWebRequestPayload {
|
||||
prompt: string;
|
||||
model: string;
|
||||
timezone: string;
|
||||
personalized_styles: Array<{
|
||||
type: string;
|
||||
key: string;
|
||||
name: string;
|
||||
nameKey: string;
|
||||
prompt: string;
|
||||
summary: string;
|
||||
summaryKey: string;
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
locale: string;
|
||||
tools: Array<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
input_schema?: Record<string, unknown>;
|
||||
integration_name?: string;
|
||||
is_mcp_app?: boolean;
|
||||
type?: string;
|
||||
}>;
|
||||
turn_message_uuids: {
|
||||
human_message_uuid: string;
|
||||
assistant_message_uuid: string;
|
||||
};
|
||||
attachments: unknown[];
|
||||
effort: string;
|
||||
files: unknown[];
|
||||
sync_sources: unknown[];
|
||||
rendering_mode: string;
|
||||
thinking_mode: string;
|
||||
create_conversation_params: {
|
||||
name: string;
|
||||
model: string;
|
||||
include_conversation_preferences: boolean;
|
||||
paprika_mode: unknown;
|
||||
compass_mode: unknown;
|
||||
is_temporary: boolean;
|
||||
enabled_imagine: boolean;
|
||||
tool_search_mode: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream chunk from Claude Web API
|
||||
*/
|
||||
export interface ClaudeWebStreamChunk {
|
||||
type?: string;
|
||||
index?: number;
|
||||
completion?: string;
|
||||
stop_reason?: string | null;
|
||||
model?: string;
|
||||
delta?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UUIDs for turn message tracking
|
||||
*/
|
||||
export function generateMessageUUIDs() {
|
||||
return {
|
||||
human_message_uuid: randomUUID(),
|
||||
assistant_message_uuid: randomUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default tool definitions for Claude Web API
|
||||
*/
|
||||
export function getDefaultTools(): ClaudeWebRequestPayload["tools"] {
|
||||
return [
|
||||
{
|
||||
name: "show_widget",
|
||||
description: "Display interactive widgets and visualizations",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
widget_type: {
|
||||
type: "string",
|
||||
description: "Type of widget to display",
|
||||
},
|
||||
},
|
||||
},
|
||||
integration_name: "visualize",
|
||||
is_mcp_app: true,
|
||||
},
|
||||
{
|
||||
name: "read_me",
|
||||
description: "Read and reference documents",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file_path: {
|
||||
type: "string",
|
||||
description: "Path to the file to read",
|
||||
},
|
||||
},
|
||||
},
|
||||
integration_name: "visualize",
|
||||
is_mcp_app: false,
|
||||
},
|
||||
{
|
||||
type: "web_search_v0",
|
||||
name: "web_search",
|
||||
},
|
||||
{
|
||||
type: "artifacts_v0",
|
||||
name: "artifacts",
|
||||
},
|
||||
{
|
||||
type: "repl_v0",
|
||||
name: "repl",
|
||||
},
|
||||
{ type: "widget", name: "weather_fetch" },
|
||||
{ type: "widget", name: "recipe_display_v0" },
|
||||
{ type: "widget", name: "places_map_display_v0" },
|
||||
{ type: "widget", name: "message_compose_v1" },
|
||||
{ type: "widget", name: "ask_user_input_v0" },
|
||||
{ type: "widget", name: "recommend_claude_apps" },
|
||||
{ type: "widget", name: "places_search" },
|
||||
{ type: "widget", name: "fetch_sports_data" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default personalized style
|
||||
*/
|
||||
export function getDefaultPersonalizedStyle(): ClaudeWebRequestPayload["personalized_styles"] {
|
||||
return [
|
||||
{
|
||||
type: "default",
|
||||
key: "Default",
|
||||
name: "Normal",
|
||||
nameKey: "normal_style_name",
|
||||
prompt: "Normal\n",
|
||||
summary: "Default responses from Claude",
|
||||
summaryKey: "normal_style_summary",
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform OpenAI format to Claude Web format
|
||||
*/
|
||||
export function transformToClaude(
|
||||
body: Record<string, unknown>,
|
||||
model: string
|
||||
): ClaudeWebRequestPayload {
|
||||
const messages = Array.isArray(body.messages) ? body.messages : [];
|
||||
|
||||
// Extract the last user message as the prompt
|
||||
let prompt = "";
|
||||
for (const msg of messages) {
|
||||
if (typeof msg === "object" && msg !== null) {
|
||||
const message = msg as Record<string, unknown>;
|
||||
if (message.role === "user") {
|
||||
prompt = String(message.content || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prompt.trim()) {
|
||||
throw new Error("No user message found in request");
|
||||
}
|
||||
|
||||
return {
|
||||
prompt,
|
||||
model: model || DEFAULT_CLAUDE_MODEL,
|
||||
timezone: "Asia/Jakarta",
|
||||
personalized_styles: getDefaultPersonalizedStyle(),
|
||||
locale: "en-US",
|
||||
tools: getDefaultTools(),
|
||||
turn_message_uuids: generateMessageUUIDs(),
|
||||
attachments: [],
|
||||
effort: "low",
|
||||
files: [],
|
||||
sync_sources: [],
|
||||
rendering_mode: "messages",
|
||||
thinking_mode: "off",
|
||||
create_conversation_params: {
|
||||
name: "",
|
||||
model: model || DEFAULT_CLAUDE_MODEL,
|
||||
include_conversation_preferences: true,
|
||||
paprika_mode: null,
|
||||
compass_mode: null,
|
||||
is_temporary: false,
|
||||
enabled_imagine: true,
|
||||
tool_search_mode: "auto",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Claude Web response to OpenAI format
|
||||
*/
|
||||
export function transformFromClaude(
|
||||
claudeContent: string,
|
||||
model: string,
|
||||
stopReason?: string
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: claudeContent,
|
||||
},
|
||||
finish_reason: stopReason === "end_turn" ? "stop" : null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
38
tests/unit/claude-web-executor-split.test.ts
Normal file
38
tests/unit/claude-web-executor-split.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the claude-web executor payload extraction.
|
||||
// The pure payload types + transforms + default tools/style live in the leaf
|
||||
// claude-web/payload.ts (no host state, no fetch/auth). Host imports back the
|
||||
// symbols it uses (ClaudeWebRequestPayload, transformToClaude, transformFromClaude).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "claude-web.ts");
|
||||
const LEAF = join(EXE, "claude-web/payload.ts");
|
||||
|
||||
test("leaf hosts the payload builders/transforms and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["transformToClaude", "transformFromClaude", "getDefaultTools"]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.match(src, /export interface ClaudeWebRequestPayload\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/claude-web\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the transforms back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/claude-web\/payload\.ts"/);
|
||||
});
|
||||
|
||||
test("transformToClaude builds a Claude-web payload with model + tools", async () => {
|
||||
const { transformToClaude } = await import("../../open-sse/executors/claude-web/payload.ts");
|
||||
const payload = transformToClaude(
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
"claude-sonnet-4-6"
|
||||
);
|
||||
assert.equal(typeof payload, "object");
|
||||
assert.ok(Array.isArray(payload.tools));
|
||||
});
|
||||
Reference in New Issue
Block a user