yagamil 发表于 2025-4-19 18:56:55

Julep2Api,支持十几种模型、没有限制

研究了一下 https://docs.julep.ai/introduction/julep
支持十几种模型,没有任何限制,模型还蛮多的
['mistral-large-2411', 'o1', 'text-embedding-3-large', 'vertex_ai/text-embedding-004', 'claude-3.5-haiku', 'cerebras/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b', 'magnum-v4-72b', 'voyage-multilingual-2', 'claude-3-haiku', 'gpt-4o', 'BAAI/bge-m3', 'openrouter/meta-llama/llama-4-maverick', 'openrouter/meta-llama/llama-4-scout', 'claude-3.5-sonnet', 'hermes-3-llama-3.1-70b', 'claude-3.5-sonnet-20240620', 'qwen-2.5-72b-instruct', 'l3.3-euryale-70b', 'gpt-4o-mini', 'cerebras/llama-3.3-70b', 'o1-preview', 'gemini-1.5-pro-latest', 'l3.1-euryale-70b', 'claude-3-sonnet', 'Alibaba-NLP/gte-large-en-v1.5', 'openrouter/meta-llama/llama-4-scout:free', 'llama-3.1-70b', 'eva-qwen-2.5-72b', 'claude-3.5-sonnet-20241022', 'gemini-2.0-flash', 'deepseek-chat', 'o1-mini', 'eva-llama-3.33-70b', 'gemini-2.5-pro-preview-03-25', 'gemini-1.5-pro', 'gpt-4-turbo', 'openrouter/meta-llama/llama-4-maverick:free', 'o3-mini', 'claude-3.7-sonnet', 'voyage-3', 'cerebras/llama-3.1-8b', 'claude-3-opus']

试了下找接口地址组合了一下,但似乎会收到 Internal Server Error?上代码!
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";

const JULEP_API_BASE_URL = "https://api.julep.ai/api";

// Simple function to generate a UUID (for Agent and Session creation)
function generateUuid(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(//g, function(c) {
    const r = Math.random() * 16 | 0, v = c === "x" ? r : (r & 0x3 | 0x8);
    return v.toString(16);
});
}

// List of supported models (from your provided list)
const supportedModels = [
'mistral-large-2411', 'o1', 'text-embedding-3-large', 'vertex_ai/text-embedding-004',
'claude-3.5-haiku', 'cerebras/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b',
'magnum-v4-72b', 'voyage-multilingual-2', 'claude-3-haiku', 'gpt-4o', 'BAAI/bge-m3',
'openrouter/meta-llama/llama-4-maverick', 'openrouter/meta-llama/llama-4-scout',
'claude-3.5-sonnet', 'hermes-3-llama-3.1-70b', 'claude-3.5-sonnet-20240620',
'qwen-2.5-72b-instruct', 'l3.3-euryale-70b', 'gpt-4o-mini', 'cerebras/llama-3.3-70b',
'o1-preview', 'gemini-1.5-pro-latest', 'l3.1-euryale-70b', 'claude-3-sonnet',
'Alibaba-NLP/gte-large-en-v1.5', 'openrouter/meta-llama/llama-4-scout:free',
'llama-3.1-70b', 'eva-qwen-2.5-72b', 'claude-3.5-sonnet-20241022', 'gemini-2.0-flash',
'deepseek-chat', 'o1-mini', 'eva-llama-3.33-70b', 'gemini-2.5-pro-preview-03-25',
'gemini-1.5-pro', 'gpt-4-turbo', 'openrouter/meta-llama/llama-4-maverick:free',
'o3-mini', 'claude-3.7-sonnet', 'voyage-3', 'cerebras/llama-3.1-8b', 'claude-3-opus'
];

async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);

// Handle /v1/models endpoint
if (url.pathname === "/v1/models") {
    if (req.method !== "GET") {
      return new Response("Method Not Allowed", { status: 405 });
    }
    return listModelsHandler();
}

// Handle /v1/chat/completions endpoint
if (url.pathname === "/v1/chat/completions") {
    if (req.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }
    return chatCompletionsHandler(req);
}

// Return 404 for other paths
return new Response("Not Found", { status: 404 });
}

// Handler for the /v1/models endpoint
function listModelsHandler(): Response {
const models = supportedModels.map(modelId => ({
    id: modelId,
    object: "model",
    created: Math.floor(Date.now() / 1000), // Use current time
    owned_by: "julep-proxy", // Indicate this proxy owns the listing
    // Add other relevant fields if needed, like permission
}));

const responseBody = {
    object: "list",
    data: models,
};

return new Response(JSON.stringify(responseBody), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
});
}

// Handler for the /v1/chat/completions endpoint
async function chatCompletionsHandler(req: Request): Promise<Response> {
const headers = new Headers(req.headers);
headers.delete("content-length");

if (!headers.has("Authorization")) {
    return new Response("Authorization header is required.", { status: 401 });
}

try {
    const openaiPayload = await req.json();

    // 1. Create a new Agent for this session
    const agentId = generateUuid();
    const createAgentPayload = {
      name: `temp-agent-${agentId}`, // Use a unique name
      about: "Temporary agent created for a chat session."
      // Add other default agent properties if desired
    };
    const createAgentResponse = await fetch(`${JULEP_API_BASE_URL}/agents/${agentId}`, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(createAgentPayload),
    });

    if (!createAgentResponse.ok) {
      console.error("Failed to create agent:", await createAgentResponse.text());
      return new Response("Failed to initialize chat session (agent creation failed).", { status: createAgentResponse.status });
    }
    // const agent = await createAgentResponse.json(); // Agent details if needed

    // 2. Create a new Session using the created Agent
    const sessionId = generateUuid(); // Use UUID for Julep Session ID path parameter
    const createSessionPayload = {
      agent: agentId, // Link the session to the newly created agent
      // Add other default session properties if desired
      // user: "user-id-if-known", // Optionally link to a user
    };

   // If the client provided a model, include it in the session creation request
    if (openaiPayload.model) {
      // Note: Julep's session creation doesn't directly take a model parameter,
      // the model is typically associated with the Agent.
      // If you need to force a specific model, you might need to update the Agent
      // after creation, or rely on the chat request's model parameter.
      // For now, we'll just create the session and let the chat request specify the model.
    }

    const createSessionResponse = await fetch(`${JULEP_API_BASE_URL}/sessions/${sessionId}`, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(createSessionPayload),
    });

    if (!createSessionResponse.ok) {
      console.error("Failed to create session:", await createSessionResponse.text());
      // Clean up the created agent? This adds complexity. For now, just fail.
      return new Response("Failed to initialize chat session (session creation failed).", { status: createSessionResponse.status });
    }
    // const session = await createSessionResponse.json(); // Session details if needed

    // 3. Initiate the chat using the new session ID (which is now a UUID)
    const julepPayload = convertOpenaiToJulep(openaiPayload);

    const julepUrl = `${JULEP_API_BASE_URL}/sessions/${sessionId}/chat`; // Use the UUID session ID here

    const julepResponse = await fetch(julepUrl, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(julepPayload),
    });

    // Handle streaming response
    if (openaiPayload.stream && julepResponse.headers.get("content-type")?.includes("text/event-stream")) {
      const readableStream = new ReadableStream({
      async start(controller) {
          const reader = julepResponse.body?.getReader();
          if (!reader) {
            controller.error("Failed to get reader from Julep response.");
            return;
          }

          const decoder = new TextDecoder();
          let buffer = "";

          try {
            while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            buffer += decoder.decode(value, { stream: true });

            // Process complete lines (events)
            const lines = buffer.split('\n');
            buffer = lines.pop() || ""; // Keep the last potentially incomplete line

            for (const line of lines) {
                if (line.startsWith("data:")) {
                  const data = line.substring(5).trim(); // Extract data after "data:"
                  if (data === "") {
                  controller.enqueue(`data: \n\n`);
                  } else {
                  try {
                      const julepChunk = JSON.parse(data);
                      // Use the Julep session ID (UUID) as the OpenAI response ID
                      const openaiChunk = convertJulepChunkToOpenai(julepChunk, openaiPayload.model || "julep-model", sessionId);
                      controller.enqueue(`data: ${JSON.stringify(openaiChunk)}\n\n`);
                  } catch (parseError) {
                      console.error("Error parsing Julep stream chunk:", parseError);
                      // Optionally enqueue an error chunk or log it
                  }
                  }
                } else if (line.startsWith(":")) {
                  // Ignore comments
                } else {
                  // Handle other event types if necessary, though "data" is typical for chat streams
                  console.warn("Received unexpected stream line:", line);
                }
            }
            }
          } catch (error) {
            console.error("Error reading Julep stream:", error);
            controller.error(error);
          } finally {
            reader.releaseLock();
            controller.close();
          }
      },
      });

      return new Response(readableStream, {
      status: julepResponse.status,
      headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Connection": "keep-alive",
      },
      });

    } else {
      // Handle non-streaming response
      const julepData = await julepResponse.json();
       // Use the Julep session ID (UUID) as the OpenAI response ID
      const openaiResponse = convertJulepToOpenai(julepData, openaiPayload.model || "julep-model", sessionId);

      return new Response(JSON.stringify(openaiResponse), {
      status: julepResponse.status,
      headers: {
          "Content-Type": "application/json",
      },
      });
    }

} catch (error) {
    console.error("Error processing request:", error);
    return new Response("Internal Server Error", { status: 500 });
}
}


function convertOpenaiToJulep(openaiPayload: any): any {
const julepPayload: any = {
    messages: openaiPayload.messages.map((msg: any) => ({
      role: msg.role,
      content: msg.content,
      name: msg.name,
      tool_call_id: msg.tool_call_id,
      tool_calls: msg.tool_calls ? msg.tool_calls.map((tc: any) => ({
      type: tc.type,
      function: tc.function ? {
          name: tc.function.name,
          arguments: tc.function.arguments
      } : undefined,
      integration: tc.integration,
      system: tc.system,
      api_call: tc.api_call,
      computer_20241022: tc.computer_20241022,
      text_editor_20241022: tc.text_editor_20241022,
      bash_20241022: tc.bash_20241022,
      id: tc.id,
      })) : undefined,
    })),
    tools: openaiPayload.tools,
    tool_choice: openaiPayload.tool_choice,
    recall: openaiPayload.recall,
    save: openaiPayload.save,
    model: openaiPayload.model,
    stream: openaiPayload.stream,
    stop: openaiPayload.stop,
    seed: openaiPayload.seed,
    max_tokens: openaiPayload.max_tokens,
    logit_bias: openaiPayload.logit_bias,
    response_format: openaiPayload.response_format,
    agent: openaiPayload.agent,
    repetition_penalty: openaiPayload.repetition_penalty,
    length_penalty: openaiPayload.length_penalty,
    min_p: openaiPayload.min_p,
    frequency_penalty: openaiPayload.frequency_penalty,
    presence_penalty: openaiPayload.presence_penalty,
    temperature: openaiPayload.temperature,
    top_p: openaiPayload.top_p,
};

// Remove session_id from the payload if it was mistakenly included
delete julepPayload.session_id;

return julepPayload;
}

// Converts a non-streaming Julep response to OpenAI format
// Added sessionId parameter to use as the OpenAI response ID
function convertJulepToOpenai(julepData: any, model: string, sessionId: string): any {
const openaiResponse: any = {
    id: sessionId, // Use the Julep session ID (UUID) as the OpenAI response ID
    object: "chat.completion",
    created: Math.floor(new Date(julepData.created_at).getTime() / 1000),
    model: model,
    choices: julepData.choices.map((choice: any) => ({
      index: choice.index,
      // *** Fix: Extract message content from choice.message.content ***
      message: {
      role: choice.message?.role || "assistant", // Use choice.message.role
      content: choice.message?.content || "", // Use choice.message.content
      tool_calls: choice.message?.tool_calls ? choice.message.tool_calls.map((tc: any) => ({
            id: tc.id,
            type: tc.type,
            function: tc.function ? {
                name: tc.function.name,
                arguments: tc.function.arguments
            } : undefined,
      })) : undefined,
      },
      finish_reason: choice.finish_reason,
      // logprobs are not typically in non-streaming response choices
    })),
    usage: julepData.usage ? {
      prompt_tokens: julepData.usage.prompt_tokens,
      completion_tokens: julepData.usage.completion_tokens,
      total_tokens: julepData.usage.total_tokens,
    } : undefined,
};

return openaiResponse;
}

// Converts a single Julep streaming chunk to OpenAI streaming format
function convertJulepChunkToOpenai(julepChunk: any, model: string, sessionId: string): any {
    const openaiChunk: any = {
      id: sessionId,
      object: "chat.completion.chunk",
      created: Math.floor(Date.now() / 1000),
      model: model,
      choices: julepChunk.choices.map((choice: any) => {
            const openaiChoice: any = {
                index: choice.index,
                // Delta structure for streaming chunks
                delta: {
                  role: choice.delta?.role,
                  content: choice.delta?.content,
                  tool_calls: choice.delta?.tool_calls ? choice.delta.tool_calls.map((tc: any) => ({
                        id: tc.id,
                        type: tc.type,
                        function: tc.function ? {
                            name: tc.function.name,
                            arguments: tc.function.arguments
                        } : undefined,
                  })) : undefined,
                },
                finish_reason: choice.finish_reason,
                // logprobs are not typically in streaming chunks
            };

            // Clean up empty delta fields
            if (openaiChoice.delta.role === undefined) delete openaiChoice.delta.role;
            if (openaiChoice.delta.content === undefined) delete openaiChoice.delta.content;
            if (openaiChoice.delta.tool_calls === undefined) delete openaiChoice.delta.tool_calls;
            if (Object.keys(openaiChoice.delta).length === 0 && openaiChoice.finish_reason === undefined) {
               delete openaiChoice.delta;
            }

            return openaiChoice;
      }),
    };

    return openaiChunk;
}


// Start the server
serve(handler);
自带 API Key,在 https://dashboard.julep.ai/ 点右下角按钮获取
页: [1]
查看完整版本: Julep2Api,支持十几种模型、没有限制