Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ai_agents/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o
OPENAI_PROXY_URL=

# Extension: orcarouter_llm2_python
# OrcaRouter API key (keys start with sk-orca-)
ORCAROUTER_API_KEY=
# Router or model ID, e.g. orcarouter/auto, orcarouter/fusion, openai/gpt-4o
ORCAROUTER_MODEL=orcarouter/auto

# Extension: grok_python
GROK_API_BASE=https://api.x.ai/v1
GROK_API_KEY=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
{
"path": "../../../ten_packages/extension/openai_llm2_python"
},
{
"path": "../../../ten_packages/extension/orcarouter_llm2_python"
},
{
"path": "../../../ten_packages/extension/azure_tts_python"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# orcarouter_llm2_python

An extension for integrating [OrcaRouter](https://www.orcarouter.ai) into your
application. OrcaRouter is an OpenAI-compatible meta-router: a single endpoint
that fronts many upstream models and adds server-side routing plus a
per-request model-fallback chain. Because it speaks the OpenAI Chat Completions
protocol, this extension reuses the OpenAI SDK and only changes the defaults.

> Disclosure: I'm an engineer on the OrcaRouter team.

## Features

- OpenAI-compatible: chat completions, streaming, tool/function calling, and
reasoning (`<think>` blocks and `reasoning_content`) all work unchanged.
- Automatic routing: point `model` at `orcarouter/auto` and OrcaRouter picks a
live model per request (the routing strategy — cheapest / quality / balanced
/ adaptive — is configured in the OrcaRouter console).
- Model-fallback chain: pass `models` + `route: "fallback"` through request
parameters to try several models in order until one succeeds.
- Attribution: `custom_headers` forwards `HTTP-Referer` / `X-Title` so the
OrcaRouter console can report which client is calling.

## Configuration

Set your OrcaRouter API key (keys start with `sk-orca-`):

```bash
export ORCAROUTER_API_KEY="sk-orca-..."
# optional; defaults to orcarouter/auto
export ORCAROUTER_MODEL="orcarouter/auto"
```

Example model IDs: `orcarouter/auto` (per-request routing), `orcarouter/fusion`
(a Fusion panel), or any provider-prefixed model such as `openai/gpt-4o`,
`anthropic/claude-haiku-4.5`, `google/gemini-2.5-pro`. See the full catalog at
https://www.orcarouter.ai/models.

### Model-fallback chain

OrcaRouter tries each model in order until one succeeds (up to 5). Pass the
controls through the request `parameters`:

```json
{
"models": ["openai/gpt-4o", "anthropic/claude-haiku-4.5"],
"route": "fallback"
}
```

## API

Refer to the `api` definition in [manifest.json] and default values in
[property.json](property.json).

| **Property** | **Type** | **Description** |
|------------------|------------|------------------------------------------------------------------------|
| `api_key` | `string` | OrcaRouter API key (`sk-orca-...`), sent as a Bearer token |
| `base_url` | `string` | API base URL (default `https://api.orcarouter.ai/v1`) |
| `model` | `string` | Model / router ID (default `orcarouter/auto`) |
| `prompt` | `string` | Default system prompt |
| `proxy_url` | `string` | Optional HTTP(S) proxy URL |
| `custom_headers` | `object` | Extra request headers (e.g. `HTTP-Referer`, `X-Title` for attribution) |

### Data In / Data Out / Command In / Command Out

Same as the other LLM2 extensions (`text_data` in/out, `flush` command),
provided by the shared `llm-interface.json` contract.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#
#
# Agora Real Time Engagement
# OrcaRouter LLM2 Integration
# Copyright (c) 2024 Agora IO. All rights reserved.
#
#
from . import addon
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
#
# Agora Real Time Engagement
# OrcaRouter LLM2 Integration
# Copyright (c) 2024 Agora IO. All rights reserved.
#
#
from ten_runtime import (
Addon,
register_addon_as_extension,
TenEnv,
)


@register_addon_as_extension("orcarouter_llm2_python")
class OrcaRouterLLM2ExtensionAddon(Addon):

def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None:
from .extension import OrcaRouterLLM2Extension

ten_env.log_info("OrcaRouterLLM2ExtensionAddon on_create_instance")
ten_env.on_create_instance_done(OrcaRouterLLM2Extension(name), context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#
#
# Agora Real Time Engagement
# OrcaRouter LLM2 Integration
# Copyright (c) 2024 Agora IO. All rights reserved.
#
#
import asyncio
from typing import AsyncGenerator

from ten_ai_base.llm2 import AsyncLLM2BaseExtension
from ten_ai_base.struct import (
LLMRequest,
LLMRequestRetrievePrompt,
LLMResponse,
LLMResponseRetrievePrompt,
)
from ten_runtime.async_ten_env import AsyncTenEnv

from .orcarouter import OrcaRouterLLM, OrcaRouterLLM2Config


class OrcaRouterLLM2Extension(AsyncLLM2BaseExtension):
def __init__(self, name: str):
super().__init__(name)
self.memory = []
self.memory_cache = []
self.config = None
self.client = None
self.sentence_fragment = ""
self.tool_task_future: asyncio.Future | None = None
self.users_count = 0
self.last_reasoning_ts = 0

async def on_init(self, ten_env: AsyncTenEnv) -> None:
ten_env.log_info("on_init")
await super().on_init(ten_env)

async def on_start(self, async_ten_env: AsyncTenEnv) -> None:
async_ten_env.log_info("on_start")
await super().on_start(async_ten_env)
config_json, _ = await self.ten_env.get_property_to_json("")
self.config = OrcaRouterLLM2Config.model_validate_json(config_json)

# Mandatory properties
if not self.config.api_key:
async_ten_env.log_info("API key is missing, exiting on_start")
return

# Create instance
try:
self.client = OrcaRouterLLM(async_ten_env, self.config)
async_ten_env.log_info(
f"initialized with max_tokens: {self.config.max_tokens}, model: {self.config.model}"
)
except Exception as err:
async_ten_env.log_info(f"Failed to initialize OrcaRouterLLM: {err}")

async def on_stop(self, async_ten_env: AsyncTenEnv) -> None:
async_ten_env.log_info("on_stop")
await super().on_stop(async_ten_env)

async def on_deinit(self, async_ten_env: AsyncTenEnv) -> None:
async_ten_env.log_info("on_deinit")
await super().on_deinit(async_ten_env)

async def on_retrieve_prompt(
self, async_ten_env: AsyncTenEnv, request: LLMRequestRetrievePrompt
) -> LLMResponseRetrievePrompt:
"""Retrieve the current prompt from config."""
prompt = self.config.prompt if self.config else ""
async_ten_env.log_info(
f"Retrieved prompt for request_id: {request.request_id}"
)
return LLMResponseRetrievePrompt(prompt=prompt)

def on_call_chat_completion(
self, async_ten_env: AsyncTenEnv, request_input: LLMRequest
) -> AsyncGenerator[LLMResponse, None]:
return self.client.get_chat_completions(request_input)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"type": "extension",
"name": "orcarouter_llm2_python",
"version": "0.1.0",
"dependencies": [
{
"type": "system",
"name": "ten_runtime_python",
"version": "0.11"
},
{
"type": "system",
"name": "ten_ai_base",
"version": "0.7"
}
],
"package": {
"include": [
"manifest.json",
"property.json",
"**.py",
"README.md",
"pyproject.toml",
"requirements.txt"
]
},
"api": {
"interface": [
{
"import_uri": "../../system/ten_ai_base/api/llm-interface.json"
}
],
"property": {
"properties": {
"api_key": {
"type": "string"
},
"model": {
"type": "string"
},
"base_url": {
"type": "string"
},
"proxy_url": {
"type": "string"
},
"prompt": {
"type": "string"
},
"custom_headers": {
"type": "object",
"properties": {}
}
}
}
}
}
Loading
Loading