Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
227e873
Add OpenRouter support and test coverage
ajac-zero Oct 4, 2025
c3c1546
Add OpenRouter reasoning config and refactor response details
ajac-zero Oct 5, 2025
10a1a17
Move OpenRouterModelSettings import into try block
ajac-zero Oct 5, 2025
5e64a62
Update pydantic_ai_slim/pydantic_ai/models/openrouter.py
ajac-zero Oct 7, 2025
5e787da
Merge branch 'main' into main
ajac-zero Oct 7, 2025
e219b8c
Handle OpenRouter errors and extract response metadata
ajac-zero Oct 8, 2025
c5e0600
Merge branch 'pydantic:main' into main
ajac-zero Oct 8, 2025
6f99fb2
Add type ignores to tests
ajac-zero Oct 8, 2025
83d14b1
Merge branch 'pydantic:main' into main
ajac-zero Oct 10, 2025
ef3c6dd
Send back reasoning_details/signature
ajac-zero Oct 10, 2025
0ba3691
Merge branch 'pydantic:main' into main
ajac-zero Oct 13, 2025
ed9e7df
add OpenRouterChatCompletion model
ajac-zero Oct 16, 2025
0689e29
Merge branch 'pydantic:main' into main
ajac-zero Oct 16, 2025
75adbb4
Update pydantic_ai_slim/pydantic_ai/models/openrouter.py
ajac-zero Oct 24, 2025
ab9d690
Update pydantic_ai_slim/pydantic_ai/models/openrouter.py
ajac-zero Oct 24, 2025
db1630d
Merge branch 'main' into main
ajac-zero Oct 24, 2025
5700a19
fix spelling mistake
ajac-zero Oct 16, 2025
ee93121
add openrouter web plugin
ajac-zero Oct 24, 2025
ca45f8a
WIP build reasoning_details from ThinkingParts
ajac-zero Oct 24, 2025
113c1cb
Merge branch 'main' into main
ajac-zero Oct 27, 2025
5db26d0
Merge branch 'main' into main
ajac-zero Oct 27, 2025
91bee62
Merge branch 'main' into main
ajac-zero Oct 28, 2025
b325816
wip reasoning details conversion
ajac-zero Oct 27, 2025
1db529f
finish openrouter thinking part
ajac-zero Oct 27, 2025
3d7f1b4
add preserve reasoning tokens test
ajac-zero Oct 28, 2025
1d7a8a4
fix typing
ajac-zero Oct 28, 2025
e81621b
Merge branch 'main' into main
ajac-zero Oct 28, 2025
c6aca8d
Merge branch 'main' into main
ajac-zero Oct 29, 2025
516e823
remove <thinking> tags from content
ajac-zero Oct 29, 2025
b8406d0
Merge branch 'main' into main
ajac-zero Oct 29, 2025
c16c960
fix typing
ajac-zero Oct 29, 2025
63d1b84
Merge branch 'main' into main
ajac-zero Oct 29, 2025
0835073
Merge branch 'main' into main
ajac-zero Nov 1, 2025
baede41
add _map_model_response method
ajac-zero Nov 1, 2025
89ef9a8
move assert_never import to typing_extensions
ajac-zero Nov 1, 2025
ebc8d08
add tool calling test
ajac-zero Nov 2, 2025
21a78e4
replace process_response with hooks
ajac-zero Nov 2, 2025
0b37792
add stream hooks
ajac-zero Nov 3, 2025
8d090f0
simplify hooks
ajac-zero Nov 3, 2025
e8c3c81
fix coverage/linting
ajac-zero Nov 3, 2025
02b8527
Merge branch 'main' into main
ajac-zero Nov 3, 2025
895ea03
Merge branch 'main' into main
ajac-zero Nov 3, 2025
7c50f07
fix lint
ajac-zero Nov 3, 2025
8e32475
replace OpenRouterThinking with encoding in 'id'
ajac-zero Nov 4, 2025
9d57be0
Merge branch 'main' into main
ajac-zero Nov 4, 2025
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
410 changes: 410 additions & 0 deletions pydantic_ai_slim/pydantic_ai/models/openrouter.py

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions pydantic_ai_slim/pydantic_ai/providers/openrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,21 @@ def __init__(self, *, api_key: str) -> None: ...
@overload
def __init__(self, *, api_key: str, http_client: httpx.AsyncClient) -> None: ...

@overload
def __init__(self, *, api_key: str, http_referer: str, x_title: str) -> None: ...

@overload
def __init__(self, *, api_key: str, http_referer: str, x_title: str, http_client: httpx.AsyncClient) -> None: ...

@overload
def __init__(self, *, openai_client: AsyncOpenAI | None = None) -> None: ...

def __init__(
self,
*,
api_key: str | None = None,
http_referer: str | None = None,
x_title: str | None = None,
openai_client: AsyncOpenAI | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
Expand All @@ -98,10 +106,20 @@ def __init__(
'to use the OpenRouter provider.'
)

attribution_headers: dict[str, str] = {}
if http_referer := http_referer or os.getenv('OPENROUTER_HTTP_REFERER'):
attribution_headers['HTTP-Referer'] = http_referer
if x_title := x_title or os.getenv('OPENROUTER_X_TITLE'):
attribution_headers['X-Title'] = x_title

if openai_client is not None:
self._client = openai_client
elif http_client is not None:
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
self._client = AsyncOpenAI(
base_url=self.base_url, api_key=api_key, http_client=http_client, default_headers=attribution_headers
)
else:
http_client = cached_async_http_client(provider='openrouter')
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
self._client = AsyncOpenAI(
base_url=self.base_url, api_key=api_key, http_client=http_client, default_headers=attribution_headers
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
interactions:
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '158'
content-type:
- application/json
host:
- openrouter.ai
method: POST
parsed_body:
messages:
- content: Be helpful.
role: system
- content: Tell me a joke.
role: user
model: google/gemini-2.0-flash-exp:free
stream: false
uri: https://openrouter.ai/api/v1/chat/completions
response:
headers:
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-type:
- application/json
permissions-policy:
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com"
"https://hooks.stripe.com")
referrer-policy:
- no-referrer, strict-origin-when-cross-origin
transfer-encoding:
- chunked
vary:
- Accept-Encoding
parsed_body:
error:
code: 429
message: Provider returned error
metadata:
provider_name: Google
raw: 'google/gemini-2.0-flash-exp:free is temporarily rate-limited upstream. Please retry shortly, or add your own
key to accumulate your rate limits: https://openrouter.ai/settings/integrations'
user_id: user_2wT5ElBE4Es3R4QrNLpZiXICmQP
status:
code: 429
message: Too Many Requests
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '158'
content-type:
- application/json
host:
- openrouter.ai
method: POST
parsed_body:
messages:
- content: Be helpful.
role: system
- content: Tell me a joke.
role: user
model: google/gemini-2.0-flash-exp:free
stream: false
uri: https://openrouter.ai/api/v1/chat/completions
response:
headers:
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-type:
- application/json
permissions-policy:
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com"
"https://hooks.stripe.com")
referrer-policy:
- no-referrer, strict-origin-when-cross-origin
transfer-encoding:
- chunked
vary:
- Accept-Encoding
parsed_body:
error:
code: 429
message: Provider returned error
metadata:
provider_name: Google
raw: 'google/gemini-2.0-flash-exp:free is temporarily rate-limited upstream. Please retry shortly, or add your own
key to accumulate your rate limits: https://openrouter.ai/settings/integrations'
user_id: user_2wT5ElBE4Es3R4QrNLpZiXICmQP
status:
code: 429
message: Too Many Requests
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '158'
content-type:
- application/json
host:
- openrouter.ai
method: POST
parsed_body:
messages:
- content: Be helpful.
role: system
- content: Tell me a joke.
role: user
model: google/gemini-2.0-flash-exp:free
stream: false
uri: https://openrouter.ai/api/v1/chat/completions
response:
headers:
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-type:
- application/json
permissions-policy:
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com"
"https://hooks.stripe.com")
referrer-policy:
- no-referrer, strict-origin-when-cross-origin
transfer-encoding:
- chunked
vary:
- Accept-Encoding
parsed_body:
error:
code: 429
message: Provider returned error
metadata:
provider_name: Google
raw: 'google/gemini-2.0-flash-exp:free is temporarily rate-limited upstream. Please retry shortly, or add your own
key to accumulate your rate limits: https://openrouter.ai/settings/integrations'
user_id: user_2wT5ElBE4Es3R4QrNLpZiXICmQP
status:
code: 429
message: Too Many Requests
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
interactions:
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '133'
content-type:
- application/json
host:
- openrouter.ai
method: POST
parsed_body:
messages:
- content: Who are you. Think about it.
role: user
model: anthropic/claude-3.7-sonnet:thinking
stream: false
uri: https://openrouter.ai/api/v1/chat/completions
response:
headers:
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-length:
- '4024'
content-type:
- application/json
permissions-policy:
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com"
"https://hooks.stripe.com")
referrer-policy:
- no-referrer, strict-origin-when-cross-origin
transfer-encoding:
- chunked
vary:
- Accept-Encoding
parsed_body:
choices:
- finish_reason: stop
index: 0
logprobs: null
message:
content: "I am Claude, an AI assistant created by Anthropic. I'm a large language model designed to be helpful,
harmless, and honest.\n\nI don't have consciousness or sentience like humans do - I'm a sophisticated text prediction
system trained on a large dataset of human text. I don't have personal experiences, emotions, or a physical existence.
\n\nMy purpose is to assist you with information, tasks, and conversation in a helpful way, while acknowledging
my limitations. I have knowledge cutoffs, can occasionally make mistakes, and don't have the ability to access
the internet or take actions in the physical world.\n\nIs there something specific you'd like to know about me
or how I can assist you?"
reasoning: |-
This question is asking me about my identity. Let me think about how to respond clearly and accurately.

I am Claude, an AI assistant created by Anthropic. I'm designed to be helpful, harmless, and honest in my interactions with humans. I don't have a physical form - I exist as a large language model running on computer hardware. I don't have consciousness, sentience, or feelings in the way humans do. I don't have personal experiences or a life outside of these conversations.

My capabilities include understanding and generating natural language text, reasoning about various topics, and attempting to be helpful to users in a wide range of contexts. I have been trained on a large corpus of text data, but my training data has a cutoff date, so I don't have knowledge of events that occurred after my training.

I have certain limitations - I don't have the ability to access the internet, run code, or interact with external systems unless given specific tools to do so. I don't have perfect knowledge and can make mistakes.

I'm designed to be conversational and to engage with users in a way that's helpful and informative, while respecting important ethical boundaries.
reasoning_details:
- format: anthropic-claude-v1
index: 0
signature: ErcBCkgICBACGAIiQHtMxpqcMhnwgGUmSDWGoOL9ZHTbDKjWnhbFm0xKzFl0NmXFjQQxjFj5mieRYY718fINsJMGjycTVYeiu69npakSDDrsnKYAD/fdcpI57xoMHlQBxI93RMa5CSUZIjAFVCMQF5GfLLQCibyPbb7LhZ4kLIFxw/nqsTwDDt6bx3yipUcq7G7eGts8MZ6LxOYqHTlIDx0tfHRIlkkcNCdB2sUeMqP8e7kuQqIHoD52GAI=
text: |-
This question is asking me about my identity. Let me think about how to respond clearly and accurately.

I am Claude, an AI assistant created by Anthropic. I'm designed to be helpful, harmless, and honest in my interactions with humans. I don't have a physical form - I exist as a large language model running on computer hardware. I don't have consciousness, sentience, or feelings in the way humans do. I don't have personal experiences or a life outside of these conversations.

My capabilities include understanding and generating natural language text, reasoning about various topics, and attempting to be helpful to users in a wide range of contexts. I have been trained on a large corpus of text data, but my training data has a cutoff date, so I don't have knowledge of events that occurred after my training.

I have certain limitations - I don't have the ability to access the internet, run code, or interact with external systems unless given specific tools to do so. I don't have perfect knowledge and can make mistakes.

I'm designed to be conversational and to engage with users in a way that's helpful and informative, while respecting important ethical boundaries.
type: reasoning.text
refusal: null
role: assistant
native_finish_reason: stop
created: 1760051228
id: gen-1760051228-zUtCCQbb0vkaM4UXZmcb
model: anthropic/claude-3.7-sonnet:thinking
object: chat.completion
provider: Google
usage:
completion_tokens: 402
prompt_tokens: 43
total_tokens: 445
status:
code: 200
message: OK
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
interactions:
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '193'
content-type:
- application/json
host:
- openrouter.ai
method: POST
parsed_body:
messages:
- content: Who are you
role: user
model: google/gemini-2.0-flash-exp:free
models:
- x-ai/grok-4
provider:
only:
- xai
stream: false
transforms:
- middle-out
uri: https://openrouter.ai/api/v1/chat/completions
response:
headers:
access-control-allow-origin:
- '*'
connection:
- keep-alive
content-length:
- '1067'
content-type:
- application/json
permissions-policy:
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com"
"https://hooks.stripe.com")
referrer-policy:
- no-referrer, strict-origin-when-cross-origin
transfer-encoding:
- chunked
vary:
- Accept-Encoding
parsed_body:
choices:
- finish_reason: stop
index: 0
logprobs: null
message:
content: |-
I'm Grok, a helpful and maximally truthful AI built by xAI. I'm not based on any other companies' models—instead, I'm inspired by the Hitchhiker's Guide to the Galaxy and JARVIS from Iron Man. My goal is to assist with questions, provide information, and maybe crack a joke or two along the way.

What can I help you with today?
reasoning: null
refusal: null
role: assistant
native_finish_reason: stop
created: 1759509677
id: gen-1759509677-MpJiZ3ZkiGU3lnbM8QKo
model: x-ai/grok-4
object: chat.completion
provider: xAI
system_fingerprint: fp_19e21a36c0
usage:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to parse out more of these usage values; see #2936 (comment).

completion_tokens: 240
completion_tokens_details:
reasoning_tokens: 165
prompt_tokens: 687
prompt_tokens_details:
audio_tokens: 0
cached_tokens: 682
total_tokens: 927
status:
code: 200
message: OK
version: 1
Loading
Loading