chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .src.python.cerebras_tool import CerebrasTool
|
||||
|
||||
__all__ = ["CerebrasTool"]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"CEREBRAS_API_KEY": null,
|
||||
"CEREBRAS_MODEL": "zai-glm-4.7"
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network, NetworkError } from '@sdk/network'
|
||||
|
||||
// Hardcoded default settings for Cerebras tool
|
||||
const CEREBRAS_API_KEY: string | null = null
|
||||
const CEREBRAS_MODEL = 'zai-glm-4.7'
|
||||
const DEFAULT_SETTINGS: Record<string, unknown> = {
|
||||
CEREBRAS_API_KEY,
|
||||
CEREBRAS_MODEL
|
||||
}
|
||||
const REQUIRED_SETTINGS = ['CEREBRAS_API_KEY']
|
||||
|
||||
interface ChatMessage {
|
||||
role: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatCompletionOptions {
|
||||
messages: ChatMessage[]
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions {
|
||||
prompt: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema: Record<string, any>
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any
|
||||
model_used?: string
|
||||
error?: string
|
||||
status_code?: number
|
||||
}
|
||||
|
||||
export default class CerebrasTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private api_key: string | null
|
||||
private model: string
|
||||
private readonly network: Network
|
||||
|
||||
// Popular Cerebras-hosted models (override with full model IDs if needed)
|
||||
private readonly popular_models = {
|
||||
'zai-glm-4.7': 'zai-glm-4.7',
|
||||
'qwen-3-235b-a22b-instruct-2507': 'qwen-3-235b-a22b-instruct-2507',
|
||||
'qwen-3-32b': 'qwen-3-32b'
|
||||
}
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
super()
|
||||
// Load configuration from central toolkits directory
|
||||
this.config = ToolkitConfig.load(CerebrasTool.TOOLKIT, this.toolName)
|
||||
|
||||
const toolSettings = ToolkitConfig.loadToolSettings(
|
||||
CerebrasTool.TOOLKIT,
|
||||
this.toolName,
|
||||
DEFAULT_SETTINGS
|
||||
)
|
||||
this.settings = toolSettings
|
||||
this.requiredSettings = REQUIRED_SETTINGS
|
||||
this.checkRequiredSettings(this.toolName)
|
||||
|
||||
// Priority: skill-provided apiKey > toolkit settings > hardcoded default
|
||||
this.api_key =
|
||||
apiKey ||
|
||||
(this.settings['CEREBRAS_API_KEY'] as string) ||
|
||||
CEREBRAS_API_KEY
|
||||
|
||||
// Load model from toolkit settings or hardcoded default
|
||||
this.model = (this.settings['CEREBRAS_MODEL'] as string) || CEREBRAS_MODEL
|
||||
|
||||
this.network = new Network({ baseURL: 'https://api.cerebras.ai/v1' })
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'cerebras'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return CerebrasTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Cerebras API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.api_key = apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of popular available models
|
||||
*/
|
||||
getAvailableModels(): string[] {
|
||||
return Object.keys(this.popular_models)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert friendly model name to Cerebras model ID
|
||||
*/
|
||||
getModelId(modelName: string): string {
|
||||
return (
|
||||
this.popular_models[modelName as keyof typeof this.popular_models] ||
|
||||
modelName
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request to Cerebras
|
||||
*/
|
||||
async chatCompletion(options: ChatCompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cerebras API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
// Use default model if none provided
|
||||
const finalModel = model || this.model
|
||||
const modelId = this.getModelId(finalModel)
|
||||
|
||||
const requestMessages = []
|
||||
if (system_prompt) {
|
||||
requestMessages.push({ role: 'system', content: system_prompt })
|
||||
}
|
||||
requestMessages.push(...messages)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const payload: any = {
|
||||
model: modelId,
|
||||
messages: requestMessages,
|
||||
temperature
|
||||
}
|
||||
|
||||
if (max_tokens) {
|
||||
payload.max_tokens = max_tokens
|
||||
}
|
||||
|
||||
if (use_structured_output) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
if (json_schema) {
|
||||
const schemaText = JSON.stringify(json_schema)
|
||||
const schemaPrompt = `You must return a valid JSON object that matches this schema:\n${schemaText}`
|
||||
payload.messages = [
|
||||
{ role: 'system', content: schemaPrompt },
|
||||
...requestMessages
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: payload
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: response.data as any,
|
||||
model_used: modelId
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cerebras API error: ${(error as Error).message}`,
|
||||
status_code:
|
||||
error instanceof NetworkError ? error.response.statusCode : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General text completion for any use case
|
||||
*/
|
||||
async completion(options: CompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { content },
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured JSON output using Cerebras structured outputs
|
||||
*/
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
json_schema,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output: true,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
const parsedData = JSON.parse(content)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedData,
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse JSON response: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available models from Cerebras API
|
||||
*/
|
||||
async listModels(): Promise<ApiResponse> {
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cerebras API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/models',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: { models: (response.data as any).data }
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to fetch models: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './cerebras-tool'
|
||||
@@ -0,0 +1,290 @@
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
from bridges.python.src.sdk.network import Network, NetworkError
|
||||
|
||||
# Hardcoded default settings for Cerebras tool
|
||||
CEREBRAS_API_KEY = None
|
||||
CEREBRAS_MODEL = "zai-glm-4.7"
|
||||
DEFAULT_SETTINGS = {
|
||||
"CEREBRAS_API_KEY": CEREBRAS_API_KEY,
|
||||
"CEREBRAS_MODEL": CEREBRAS_MODEL,
|
||||
}
|
||||
REQUIRED_SETTINGS = ["CEREBRAS_API_KEY"]
|
||||
|
||||
|
||||
class CerebrasTool(BaseTool):
|
||||
"""Cerebras tool for LLM API access (e.g., GLM 4.7)"""
|
||||
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
|
||||
tool_settings = ToolkitConfig.load_tool_settings(
|
||||
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
|
||||
)
|
||||
self.settings = tool_settings
|
||||
self.required_settings = REQUIRED_SETTINGS
|
||||
self._check_required_settings(self.tool_name)
|
||||
|
||||
# Priority: skill-provided api_key > toolkit settings > hardcoded default
|
||||
self.api_key = api_key or self.settings.get(
|
||||
"CEREBRAS_API_KEY", CEREBRAS_API_KEY
|
||||
)
|
||||
|
||||
# Load model settings
|
||||
self.model = self.settings.get("CEREBRAS_MODEL", CEREBRAS_MODEL)
|
||||
|
||||
self.network = Network({"base_url": "https://api.cerebras.ai/v1"})
|
||||
|
||||
# Popular Cerebras-hosted models (override with full model IDs if needed)
|
||||
self.popular_models = {
|
||||
"zai-glm-4.7": "zai-glm-4.7",
|
||||
"qwen-3-235b-a22b-instruct-2507": "qwen-3-235b-a22b-instruct-2507",
|
||||
"qwen-3-32b": "qwen-3-32b",
|
||||
}
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "cerebras"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config["description"]
|
||||
|
||||
def set_api_key(self, api_key: str) -> None:
|
||||
"""Set the Cerebras API key"""
|
||||
self.api_key = api_key
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
"""Get list of popular available models"""
|
||||
return list(self.popular_models.keys())
|
||||
|
||||
def get_model_id(self, model_name: str) -> str:
|
||||
"""Convert friendly model name to Cerebras model ID"""
|
||||
return self.popular_models.get(model_name, model_name)
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a chat completion request to Cerebras
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
model: Model name (friendly name or full model ID)
|
||||
temperature: Sampling temperature (0-2)
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: System prompt to prepend
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output (required if use_structured_output=True)
|
||||
|
||||
Returns:
|
||||
Dict with response data or error information
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "Cerebras API key not configured"}
|
||||
|
||||
# Use default model if none provided
|
||||
model = model or self.model
|
||||
|
||||
model_id = self.get_model_id(model)
|
||||
|
||||
request_messages: List[Dict[str, str]] = []
|
||||
if system_prompt:
|
||||
request_messages.append({"role": "system", "content": system_prompt})
|
||||
request_messages.extend(messages)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model_id,
|
||||
"messages": request_messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
if use_structured_output:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
if json_schema:
|
||||
schema_text = json.dumps(json_schema)
|
||||
schema_prompt = (
|
||||
"You must return a valid JSON object that matches this schema:\n"
|
||||
f"{schema_text}"
|
||||
)
|
||||
payload["messages"] = [
|
||||
{"role": "system", "content": schema_prompt}
|
||||
] + request_messages
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/chat/completions",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"data": payload,
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "data": response["data"], "model_used": model_id}
|
||||
|
||||
except NetworkError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Cerebras API error: {str(e)}",
|
||||
"status_code": getattr(e.response, "status_code", None),
|
||||
}
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
General text completion for any use case
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
model: LLM model to use
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output
|
||||
|
||||
Returns:
|
||||
Dict with completion result
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=use_structured_output,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate structured JSON output using Cerebras structured outputs
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
json_schema: JSON schema defining the required output structure
|
||||
model: LLM model to use
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
|
||||
Returns:
|
||||
Dict with parsed JSON result or error
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=True,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
parsed_data = json.loads(content)
|
||||
return {
|
||||
"success": True,
|
||||
"data": parsed_data,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to parse JSON response: {str(e)}",
|
||||
}
|
||||
|
||||
def list_models(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get list of available models from Cerebras API
|
||||
|
||||
Returns:
|
||||
Dict with models list or error
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "Cerebras API key not configured"}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/models",
|
||||
"method": "GET",
|
||||
"headers": {"Authorization": f"Bearer {self.api_key}"},
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"models": response["data"].get("data", response["data"]),
|
||||
}
|
||||
except NetworkError as e:
|
||||
return {"success": False, "error": f"Failed to fetch models: {str(e)}"}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "cerebras",
|
||||
"toolkit_id": "communication",
|
||||
"name": "Cerebras",
|
||||
"description": "A tool for interacting with Cerebras LLM APIs (e.g., GLM 4.7).",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"chatCompletion": {
|
||||
"description": "Generate a chat completion using the Cerebras API.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"messages"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"description": "Generate a completion using the Cerebras API.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"listModels": {
|
||||
"description": "List available Cerebras models.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.inference_tool import InferenceTool
|
||||
|
||||
__all__ = ["InferenceTool"]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './inference-tool'
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network } from '@sdk/network'
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
system_prompt?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
thought_tokens_budget?: number
|
||||
disable_thinking?: boolean
|
||||
reasoning_mode?: 'off' | 'guarded' | 'on'
|
||||
track_provider_errors?: boolean
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions extends CompletionOptions {
|
||||
json_schema: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface InferenceResponse {
|
||||
success: boolean
|
||||
output?: unknown
|
||||
reasoning?: string
|
||||
usedInputTokens?: number
|
||||
usedOutputTokens?: number
|
||||
generationDurationMs?: number
|
||||
providerDecodeDurationMs?: number
|
||||
providerTokensPerSecond?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default class InferenceTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private readonly network: Network
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.config = ToolkitConfig.load(InferenceTool.TOOLKIT, this.toolName)
|
||||
this.network = new Network({
|
||||
baseURL: `${process.env['LEON_HOST']}:${process.env['LEON_PORT']}/api/v1`
|
||||
})
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'inference'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return InferenceTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
async completion(options: CompletionOptions): Promise<InferenceResponse> {
|
||||
const response = await this.network.request<InferenceResponse>({
|
||||
url: '/inference',
|
||||
method: 'POST',
|
||||
data: {
|
||||
prompt: options.prompt,
|
||||
systemPrompt: options.system_prompt,
|
||||
temperature: options.temperature,
|
||||
maxTokens: options.max_tokens,
|
||||
thoughtTokensBudget: options.thought_tokens_budget,
|
||||
disableThinking: options.disable_thinking,
|
||||
reasoningMode: options.reasoning_mode,
|
||||
trackProviderErrors: options.track_provider_errors
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<InferenceResponse> {
|
||||
const response = await this.network.request<InferenceResponse>({
|
||||
url: '/inference',
|
||||
method: 'POST',
|
||||
data: {
|
||||
prompt: options.prompt,
|
||||
systemPrompt: options.system_prompt,
|
||||
temperature: options.temperature,
|
||||
maxTokens: options.max_tokens,
|
||||
thoughtTokensBudget: options.thought_tokens_budget,
|
||||
jsonSchema: options.json_schema,
|
||||
disableThinking: options.disable_thinking,
|
||||
reasoningMode: options.reasoning_mode,
|
||||
trackProviderErrors: options.track_provider_errors
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional, Literal
|
||||
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.network import Network
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
|
||||
|
||||
class InferenceTool(BaseTool):
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
self.network = Network(
|
||||
{
|
||||
"base_url": f"{os.environ.get('LEON_HOST')}:{os.environ.get('LEON_PORT')}/api/v1"
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "inference"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config.get("description", "")
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
thought_tokens_budget: Optional[int] = None,
|
||||
disable_thinking: Optional[bool] = None,
|
||||
reasoning_mode: Optional[Literal["off", "guarded", "on"]] = None,
|
||||
track_provider_errors: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/inference",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"prompt": prompt,
|
||||
"systemPrompt": system_prompt,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens,
|
||||
"thoughtTokensBudget": thought_tokens_budget,
|
||||
"disableThinking": disable_thinking,
|
||||
"reasoningMode": reasoning_mode,
|
||||
"trackProviderErrors": track_provider_errors,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response["data"]
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
thought_tokens_budget: Optional[int] = None,
|
||||
disable_thinking: Optional[bool] = None,
|
||||
reasoning_mode: Optional[Literal["off", "guarded", "on"]] = None,
|
||||
track_provider_errors: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/inference",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"prompt": prompt,
|
||||
"systemPrompt": system_prompt,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens,
|
||||
"thoughtTokensBudget": thought_tokens_budget,
|
||||
"jsonSchema": json_schema,
|
||||
"disableThinking": disable_thinking,
|
||||
"reasoningMode": reasoning_mode,
|
||||
"trackProviderErrors": track_provider_errors,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response["data"]
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "inference",
|
||||
"toolkit_id": "communication",
|
||||
"name": "Inference",
|
||||
"description": "A generic Leon workflow inference tool backed by the active workflow LLM provider.",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"completion": {
|
||||
"description": "Generate a workflow inference completion.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"thought_tokens_budget": {
|
||||
"type": "number"
|
||||
},
|
||||
"disable_thinking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"track_provider_errors": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured workflow inference completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"thought_tokens_budget": {
|
||||
"type": "number"
|
||||
},
|
||||
"disable_thinking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"track_provider_errors": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.openrouter_tool import OpenRouterTool
|
||||
|
||||
__all__ = ["OpenRouterTool"]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"OPENROUTER_API_KEY": null,
|
||||
"OPENROUTER_MODEL": "google/gemini-3.1-flash-lite"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './openrouter-tool'
|
||||
@@ -0,0 +1,340 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network, NetworkError } from '@sdk/network'
|
||||
|
||||
// Hardcoded default settings for OpenRouter tool
|
||||
const OPENROUTER_API_KEY: string | null = null
|
||||
const OPENROUTER_MODEL = 'google/gemini-3.1-flash-lite'
|
||||
const DEFAULT_SETTINGS: Record<string, unknown> = {
|
||||
OPENROUTER_API_KEY,
|
||||
OPENROUTER_MODEL
|
||||
}
|
||||
const REQUIRED_SETTINGS = ['OPENROUTER_API_KEY']
|
||||
|
||||
interface ChatMessage {
|
||||
role: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatCompletionOptions {
|
||||
messages: ChatMessage[]
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions {
|
||||
prompt: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema: Record<string, any>
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any
|
||||
model_used?: string
|
||||
error?: string
|
||||
status_code?: number
|
||||
}
|
||||
|
||||
export default class OpenRouterTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private api_key: string | null
|
||||
private model: string
|
||||
private readonly network: Network
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
super()
|
||||
// Load configuration from central toolkits directory
|
||||
this.config = ToolkitConfig.load(OpenRouterTool.TOOLKIT, this.toolName)
|
||||
|
||||
const toolSettings = ToolkitConfig.loadToolSettings(
|
||||
OpenRouterTool.TOOLKIT,
|
||||
this.toolName,
|
||||
DEFAULT_SETTINGS
|
||||
)
|
||||
this.settings = toolSettings
|
||||
this.requiredSettings = apiKey ? [] : REQUIRED_SETTINGS
|
||||
this.checkRequiredSettings(this.toolName)
|
||||
|
||||
// Priority: skill-provided apiKey > toolkit settings > hardcoded default
|
||||
this.api_key =
|
||||
apiKey ||
|
||||
(this.settings['OPENROUTER_API_KEY'] as string) ||
|
||||
OPENROUTER_API_KEY
|
||||
|
||||
// Load model from toolkit settings or hardcoded default
|
||||
this.model =
|
||||
(this.settings['OPENROUTER_MODEL'] as string) || OPENROUTER_MODEL
|
||||
|
||||
this.network = new Network({ baseURL: 'https://openrouter.ai/api' })
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'openrouter'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return OpenRouterTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OpenRouter API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.api_key = apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request to OpenRouter
|
||||
*/
|
||||
async chatCompletion(options: ChatCompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'OpenRouter API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
// Use default model if none provided
|
||||
const finalModel = model || this.model
|
||||
|
||||
// Prepare messages with system prompt if provided
|
||||
const requestMessages = []
|
||||
if (system_prompt) {
|
||||
requestMessages.push({ role: 'system', content: system_prompt })
|
||||
}
|
||||
requestMessages.push(...messages)
|
||||
|
||||
// Prepare request payload
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const payload: any = {
|
||||
model: finalModel,
|
||||
messages: requestMessages,
|
||||
temperature
|
||||
}
|
||||
|
||||
if (max_tokens) {
|
||||
payload.max_tokens = max_tokens
|
||||
}
|
||||
|
||||
// Add structured output configuration if requested
|
||||
if (use_structured_output && json_schema) {
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: json_schema['name'] || 'response',
|
||||
strict: true,
|
||||
schema: json_schema['schema']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: payload
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: response.data as any,
|
||||
model_used: finalModel
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OpenRouter API error: ${(error as Error).message}`,
|
||||
status_code:
|
||||
error instanceof NetworkError ? error.response.statusCode : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General text completion for any use case
|
||||
*/
|
||||
async completion(options: CompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { content },
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured JSON output using OpenRouter's structured outputs feature
|
||||
*/
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
json_schema,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output: true,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
const parsedData =
|
||||
typeof content === 'string' ? JSON.parse(content) : content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedData,
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0]?.message?.content
|
||||
|
||||
if (error instanceof SyntaxError) {
|
||||
// Show raw response preview to help debug JSON parsing errors
|
||||
const preview =
|
||||
typeof content === 'string'
|
||||
? content.substring(0, 500)
|
||||
: JSON.stringify(content ?? 'null').substring(0, 500)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse JSON response: ${error.message}. Response preview: ${preview}`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available models from OpenRouter API
|
||||
*/
|
||||
async listModels(): Promise<ApiResponse> {
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'OpenRouter API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/v1/models',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: { models: (response.data as any).data }
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to fetch models: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
from bridges.python.src.sdk.network import Network, NetworkError
|
||||
|
||||
# Hardcoded default settings for OpenRouter tool
|
||||
OPENROUTER_API_KEY = None
|
||||
OPENROUTER_MODEL = "google/gemini-3.1-flash-lite"
|
||||
DEFAULT_SETTINGS = {
|
||||
"OPENROUTER_API_KEY": OPENROUTER_API_KEY,
|
||||
"OPENROUTER_MODEL": OPENROUTER_MODEL,
|
||||
}
|
||||
REQUIRED_SETTINGS = ["OPENROUTER_API_KEY"]
|
||||
|
||||
|
||||
class OpenRouterTool(BaseTool):
|
||||
"""OpenRouter tool for unified LLM API access across all skills"""
|
||||
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
|
||||
tool_settings = ToolkitConfig.load_tool_settings(
|
||||
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
|
||||
)
|
||||
self.settings = tool_settings
|
||||
self.required_settings = [] if api_key else REQUIRED_SETTINGS
|
||||
self._check_required_settings(self.tool_name)
|
||||
|
||||
# Priority: skill-provided api_key > toolkit settings > hardcoded default
|
||||
self.api_key = api_key or self.settings.get(
|
||||
"OPENROUTER_API_KEY", OPENROUTER_API_KEY
|
||||
)
|
||||
|
||||
# Load model settings
|
||||
self.model = self.settings.get("OPENROUTER_MODEL", OPENROUTER_MODEL)
|
||||
|
||||
self.network = Network({"base_url": "https://openrouter.ai/api"})
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "openrouter"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config["description"]
|
||||
|
||||
def set_api_key(self, api_key: str) -> None:
|
||||
"""Set the OpenRouter API key"""
|
||||
self.api_key = api_key
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a chat completion request to OpenRouter
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
model: Model ID (full OpenRouter model ID, e.g. 'google/gemini-3.1-flash-lite')
|
||||
temperature: Sampling temperature (0-2)
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: System prompt to prepend
|
||||
use_structured_output: Whether to use OpenRouter's structured outputs
|
||||
json_schema: JSON schema for structured output (required if use_structured_output=True)
|
||||
|
||||
Returns:
|
||||
Dict with response data or error information
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "OpenRouter API key not configured"}
|
||||
|
||||
# Use default model if none provided
|
||||
model = model or self.model
|
||||
|
||||
# Prepare messages with system prompt if provided
|
||||
request_messages = []
|
||||
if system_prompt:
|
||||
request_messages.append({"role": "system", "content": system_prompt})
|
||||
request_messages.extend(messages)
|
||||
|
||||
# Prepare request payload
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": request_messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
# Add structured output configuration if requested
|
||||
if use_structured_output and json_schema:
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": json_schema.get("name", "response"),
|
||||
"strict": True,
|
||||
"schema": json_schema["schema"],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/v1/chat/completions",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"data": payload,
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "data": response["data"], "model_used": model}
|
||||
|
||||
except NetworkError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"OpenRouter API error: {str(e)}",
|
||||
"status_code": getattr(e.response, "status_code", None),
|
||||
}
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
General text completion for any use case
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
model: Model ID (full OpenRouter model ID)
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output
|
||||
|
||||
Returns:
|
||||
Dict with completion result
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=use_structured_output,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate structured JSON output using OpenRouter's structured outputs feature
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
json_schema: JSON schema defining the required output structure
|
||||
model: Model ID (full OpenRouter model ID)
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
|
||||
Returns:
|
||||
Dict with parsed JSON result or error
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=True,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
# With structured outputs, content is already valid JSON
|
||||
parsed_data = json.loads(content)
|
||||
return {
|
||||
"success": True,
|
||||
"data": parsed_data,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to parse JSON response: {str(e)}",
|
||||
}
|
||||
|
||||
def list_models(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get list of available models from OpenRouter API
|
||||
|
||||
Returns:
|
||||
Dict with models list or error
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "OpenRouter API key not configured"}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/v1/models",
|
||||
"method": "GET",
|
||||
"headers": {"Authorization": f"Bearer {self.api_key}"},
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "models": response["data"]["data"]}
|
||||
|
||||
except NetworkError as e:
|
||||
return {"success": False, "error": f"Failed to fetch models: {str(e)}"}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "openrouter",
|
||||
"toolkit_id": "communication",
|
||||
"name": "OpenRouter",
|
||||
"description": "A tool for interacting with various LLMs through the OpenRouter API gateway.",
|
||||
"icon_name": "route-line",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"chatCompletion": {
|
||||
"description": "Generate a chat completion using OpenRouter.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"messages"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"description": "Generate a completion using OpenRouter.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"listModels": {
|
||||
"description": "List available OpenRouter models.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Communication",
|
||||
"description": "Tools for communication and language model interactions.",
|
||||
"icon_name": "chat-3-line",
|
||||
"context_files": [
|
||||
"LEON.md",
|
||||
"ARCHITECTURE.md",
|
||||
"MEDIA_PROFILE.md"
|
||||
],
|
||||
"tools": []
|
||||
}
|
||||
Reference in New Issue
Block a user