-
Notifications
You must be signed in to change notification settings - Fork 198
Support overriding default model provider in instrument_openai #1609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| return cast('ResponseT', response) | ||
|
|
||
| span.set_attribute('gen_ai.system', 'openai') | ||
| model_provider: str = cast(str, (span.attributes or {}).get('overridden_model_provider', "openai")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
span.attributes won't work when the span isn't recording
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki then how should i check if overridden_model_provider is set, or, as you proposed, check that gen_ai.system hasnt already been set upstream
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use getattr(span, 'attributes', None)
|
|
||
| span_data['async'] = is_async | ||
| if model_provider is not None: | ||
| span_data['overridden_model_provider'] = model_provider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather just set gen_ai.system (and gen_ai.provider.name, the new attribute) here
| get_endpoint_config_fn: Callable[[Any], EndpointConfig], | ||
| on_response_fn: Callable[[Any, LogfireSpan], Any], | ||
| is_async_client_fn: Callable[[type[Any]], bool], | ||
| model_provider: str | None = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just call it provider
| usage_data = extract_usage( | ||
| response_data, | ||
| provider_id='openai', | ||
| provider_id=model_provider, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think leave this as is, since we're using the openai client we should be able to assume the shape of the usage.
|
@alexmojaki addressed your feedback. can you quickly review it again before i add tests? |
| return cast('ResponseT', response) | ||
|
|
||
| span.set_attribute('gen_ai.system', 'openai') | ||
| if getattr(span, 'attributes', None) is None or (span.attributes or {}).get('gen_ai.system', None) is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki type checking complained that span.attributes could be None, so i had to do (span.attributes or {})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if getattr(span, 'attributes', None) is None or (span.attributes or {}).get('gen_ai.system', None) is None: | |
| if not (getattr(span, 'attributes', None) or {}).get('gen_ai.system'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doing this will incur the following type checker error
/Users/yifeiyan/Dev/logfire/logfire/_internal/integrations/llm_providers/openai.py:186:12 - error: Type of "get" is partially unknown
Type of "get" is "Any | Overload[(key: Unknown, default: None = None, /) -> (Unknown | None), (key: Unknown, default: Unknown, /) -> Unknown, (key: Unknown, default: _T@get, /) -> (Unknown | _T@get)]" (reportUnknownMemberType)
do you want me to suppress it or cast it? the most compact i can make it is
if (getattr(span, 'attributes', {})).get('gen_ai.system', None) is None:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that works too
no, please get CI passing first |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
@alexmojaki now everything passes except for code coverage and linter that complains about missing docstring for the new param. can you take a quick look now? |
|
no, please get CI passing first, and get a review from copilot if possible |
@alexmojaki the reason im nagging you is because this PR contains an API change of a public method (though a backwards compatible one). Usually, orgs are pretty protective about public API changes, so i dont like to do a much of work and then revert because they dislike the API changes. Therefore, all im asking you is a) to quickly confirm that the API change looks good, or b) to tell me that the specific API changes here are no big deal and should proceed without worries |
|
Yes it looks fine |
|
@alexmojaki all the tests have been added, and all the CI checks pass. Thanks for the patience. |
| return cast('ResponseT', response) | ||
|
|
||
| span.set_attribute('gen_ai.system', 'openai') | ||
| if getattr(span, 'attributes', None) is None or (span.attributes or {}).get('gen_ai.system', None) is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if getattr(span, 'attributes', None) is None or (span.attributes or {}).get('gen_ai.system', None) is None: | |
| if not (getattr(span, 'attributes', None) or {}).get('gen_ai.system'): |
|
Note: starting recently, running the test suite locally always fails with the same following errors I did everything as outlined in https://github.com/pydantic/logfire/blob/main/CONTRIBUTING.md |
|
You probably have a |
|
@alexmojaki you didnt submit a review last time, so re-requesting your review via this comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for overriding the default model provider in instrument_openai to enable proper attribution and cost calculation for OpenAI-compatible providers like OpenRouter.
Changes:
- Added
override_providerparameter toinstrument_openai()method with comprehensive documentation - Modified
instrument_llm_provider()to accept and useoverride_providerto set thegen_ai.systemattribute - Updated
on_response()to conditionally setgen_ai.systemto 'openai' only when not already present
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
logfire/_internal/main.py |
Added override_provider parameter to instrument_openai() with documentation explaining its purpose and benefits |
logfire/_internal/integrations/llm_providers/llm_provider.py |
Added override_provider parameter and logic to set gen_ai.system in span_data when provided |
logfire/_internal/integrations/llm_providers/openai.py |
Modified on_response() to only set gen_ai.system='openai' when not already present, allowing override |
tests/test_llm_provider.py |
Added parametrized unit tests for sync and async clients with override_provider |
tests/otel_integrations/test_openai.py |
Added comprehensive integration tests covering sync, async, streaming, default behavior, and on_response behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Please also set |
|
@alexmojaki done |
| provider = (getattr(span, 'attributes', {}) or {}).get('gen_ai.system', None) | ||
| if provider is None: | ||
| provider = 'openai' | ||
| span.set_attribute('gen_ai.system', provider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also set PROVIDER_NAME
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki are you sure? PROVIDER_NAME is already set upstream in get_endpoint_config (e.g. https://github.com/pydantic/logfire/blob/main/logfire/_internal/integrations/llm_providers/openai.py#L98) so I dont think it makes sense to redo it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, it needs to be configurable there too. gen_ai.system is deprecated and PROVIDER_NAME is replacing it. They should always be set together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but it looks like gen_ai.system hasnt been deprecated yet and PROVIDER_NAME is still used upstream. Are you just trying to be forward-looking in this PR? If so, that would expand the scope of the PR to both a) support a new feature, and b) implement parts of a future deprecation. I generally dont like this expanding scope, but if thats what you are asking me, i will do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be clear on b), you are just asking me to implement:
- this comment Support overriding default model provider in instrument_openai #1609 (comment)
- set PROVIDER_NAME in
on_response
correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's deprecated in the spec, see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#deprecated-genai-attributes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki yes, but not deprecated in Logfire. Plus, I thought Logfire's whole selling point was its opinionated implementation of OTEL, so it doesnt seem like a necessity to be up to date with OTEL.
But besides all this, I simply objected to expanding this PR's scope. I would have been happy to open a separate PR to address this; just not in this PR.
Either way, I tried to please you and implemented what you asked, and it actually expanded the scope a lot more. See this draft PR: yiphei#7 . Given the expanded scope and further back and forth, I really dont want to do this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmojaki if you agree to a stacked PR, im more amenable to that
| return cast('ResponseT', response) | ||
|
|
||
| span.set_attribute('gen_ai.system', 'openai') | ||
| provider = (getattr(span, 'attributes', {}) or {}).get('gen_ai.system', None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| provider = (getattr(span, 'attributes', {}) or {}).get('gen_ai.system', None) | |
| provider = (getattr(span, 'attributes', {}) or {}).get(PROVIDER_NAME, None) |
c702827 to
28e7ee0
Compare
Support overriding the default system provider of
openaiininstrument_openaito e.g.openrouter. This is necessary to get features like automatic cost computation. For more context: https://pydanticlogfire.slack.com/archives/C06EDRBSAH3/p1767644423267469?thread_ts=1759944637.702099&cid=C06EDRBSAH3Note:
genai_prices.calc_pricedoes not support extraction logic for Openrounter. So the try block that callsgenai_prices.calc_priceinon_responsewill error with the followingThis wont be addressed in this PR because: a) genai_prices is a separate package, so out of scope, and b) client-side computation is not necessary to see the cost info in the UI since that is also computed server-side, though you do lose explicit setting of
operation.costattributeRun this to check things works as expected