
python版本3.12pydantic-ai版本1.70.0操作系统windows11一、问题抛出作为PydanticFastAPITyper的忠实用户看到pydantic团队的新作品pydantic-ai后也是第一时间尝尝鲜了。于是本人便写下了如下demofrompydantic_aiimportAgentimportos# pydantic-ai调用moonshot模型必须配置MOONSHOTAI_API_KEY环境变量assertos.getenv(MOONSHOTAI_API_KEY)isnotNoneagentAgent(moonshotai:moonshot-v1-8k,system_prompt请介绍一下pydantic)ansagent.run_sync()print(ans.output)我兴奋地查看终端想要看到预料中的输出但是实际运行结果给我拉了坨大的raise ModelHTTPError(status_codestatus_code, model_nameself.model_name, bodye.body) from e pydantic_ai.exceptions.ModelHTTPError: status_code: 401, model_name: moonshot-v1-8k, body: {message: Invalid Authentication, type: invalid_authentication_error}这是什么意思为什么报了401呢API过期了可是用其他库写的代码都能跑通啊。于是本人便顺着TraceBack一路打断点发现了问题所在。首先大部分人可能对底层源码没什么兴趣所以本人就先果后因先直接讲解决方案有兴趣探究原理的可以自行查看。二、解决方案其实很简单不要设置MOONSHOTAI_API_KEY这个环境变量。而是设置OPENAI_API_KEY和OPENAI_BASE_URL。如下所示frompydantic_aiimportAgentimportos# pydantic-ai调用openai模型必须配置OPENAI_API_KEY环境变量# 注意配置的是kimi的api-keyassertos.getenv(OPENAI_API_KEY)isnotNone# 可以通过环境变量覆盖掉base_url# 如果不设置这个环境变量他就用的是openai的base_urlassertos.getenv(OPENAI_BASE_URL)https://api.moonshot.cn/v1agentAgent(openai:moonshot-v1-8k,system_prompt请介绍一下pydantic)ansagent.run_sync()print(ans.output)这个时候在运行就没有问题啦。好了对原因没有兴趣的朋友可以改代码去了。三、原因剖析注以下内容涉及pydantic-ai源码如果您的版本不是1.70.0可能实际内容会不一样。首先太复杂的细节我们并不需要知道我们只需要知道pydantic_ai.Agent的底层其实还是openai是封装了一个异步的openai的client这个client可以通过调用agent._model.client获得到。我们运行这个代码frompydantic_aiimportAgent agentAgent(moonshotai:moonshot-v1-8k,system_prompt请介绍一下pydantic)print(agent._model.client._base_url)他的输出是https://api.moonshot.ai/v1/至此真相已大白pydantic-ai底层调用moonshot的base_url用的是国际版的https://api.moonshot.ai/v1/而不是国内版的https://api.moonshot.cn/v1/由于各种原因国内用户通常需要访问 api.moonshot.cn而 api.moonshot.ai 是国际版端点两者账号体系不互通。那这个https://api.moonshot.ai/v1/是怎么来的我要如何把他修改成https://api.moonshot.cn/v1/很好小子我们就抽丝剥茧先弄清楚agent._model这个封装属性是怎么回事。先看Agent类的构造器其中有这样两行pydantic_ai.agent.__init__.py #L342# defer_model_check通常为False所以这里不用管这个if# 直接研究infer_model即可ifmodelisNoneordefer_model_check:self._modelmodelelse:self._modelmodels.infer_model(model)可以看到这个self._model是通过models.infer_model函数处理得到的。这个models.infer_model是怎么实现的呢这里你将看到pydantic-ai中的史诗级屎山代码pydantic_ai.models.__init__.py #L1202definfer_model(# noqa: C901model:Model|KnownModelName|str,provider_factory:Callable[[str],Provider[Any]]infer_provider)-Model:Infer the model from the name. Args: model: Model name to instantiate, in the format of provider:model. Use the string test to instantiate TestModel. provider_factory: Function that instantiates a provider object. The provider name is passed into the function parameter. Defaults to provider.infer_provider. ifisinstance(model,Model):returnmodelelifmodeltest:from.testimportTestModelreturnTestModel()provider_name,model_nameparse_model_id(model)对于我们传入的moonshotai:moonshot-v1-8kprovider_name和model_name会分别为moonshotai和moonshotai:moonshot-v1-8k接下来的代码是堪称地狱级别的if ... elif .. elif ...堆叠model_kindprovider_nameifmodel_kind.startswith(gateway/):from..providers.gatewayimportnormalize_gateway_provider model_kindnormalize_gateway_provider(model_kind)# OpenRouter and Cerebras need to be checked before OpenAI,# as they are in OpenAIChatCompatibleProvider but have their own model classes.ifmodel_kindopenrouter:from.openrouterimportOpenRouterModelreturnOpenRouterModel(model_name,providerprovider)elifmodel_kindcerebras:from.cerebrasimportCerebrasModelreturnCerebrasModel(model_name,providerprovider)elifmodel_kindin(openai-chat,openai,*get_args(OpenAIChatCompatibleProvider.__value__)):from.openaiimportOpenAIChatModelreturnOpenAIChatModel(model_name,providerprovider)elifmodel_kindopenai-responses:from.openaiimportOpenAIResponsesModelreturnOpenAIResponsesModel(model_name,providerprovider)# 这里堆叠了无数行就不展开了最后实际的返回值是哪一行返回的呢是第1252行因为moonshot使用的也是openai协议。那AsyncOpenAI封装在哪里呢查看OpenAIChatModel的构造器pydantic_ai.models.openai.py #L559def__init__(self,model_name:OpenAIModelName,*,provider:OpenAIChatCompatibleProvider|Literal[openai,openai-chat,gateway,]|Provider[AsyncOpenAI]openai,profile:ModelProfileSpec|NoneNone,system_prompt_role:OpenAISystemPromptRole|NoneNone,settings:ModelSettings|NoneNone,):Initialize an OpenAI model. Args: model_name: The name of the OpenAI model to use. List of model names available [here](https://github.com/openai/openai-python/blob/v1.54.3/src/openai/types/chat_model.py#L7) (Unfortunately, despite being ask to do so, OpenAI do not provide .inv files for their API). provider: The provider to use. Defaults to openai. profile: The model profile to use. Defaults to a profile picked by the provider based on the model name. system_prompt_role: The role to use for the system prompt message. If not provided, defaults to system. In the future, this may be inferred from the model name. settings: Default model settings for this model instance. self._model_namemodel_nameifisinstance(provider,str):providerinfer_provider(gateway/openaiifprovidergatewayelseprovider)self._providerprovider self.clientprovider.client找到你了原来AsyncOpenAI封装在这个provider里。注意pydantic_ai.models.__init__.py #L1231providerprovider_factory(provider_name)provide是这里传来的很好我们查看一下provider_factory或者说infer_provider是如何实现的pydantic_ai.providers.__init__.py #L189definfer_provider(provider:str)-Provider[Any]:Infer the provider from the provider name.ifprovider.startswith(gateway/):from.gatewayimportgateway_provider upstream_providerprovider.removeprefix(gateway/)returngateway_provider(upstream_provider)elifproviderin(google-vertex,google-gla,vertexai):from.googleimportGoogleProviderreturnGoogleProvider(vertexaiproviderin(google-vertex,vertexai))else:provider_classinfer_provider_class(provider)returnprovider_class()天呐又得跳到infer_provider_class里面看源码那就跳吧。我去……又是疯狂的if ... elif .. elif ...地狱pydantic_ai.providers.__init__.py #L55definfer_provider_class(provider:str)-type[Provider[Any]]:# noqa: C901Infers the provider class from the provider name.# Normalize gateway-prefixed providers (e.g. gateway/openai - openai)ifprovider.startswith(gateway/):from.gatewayimportnormalize_gateway_provider providernormalize_gateway_provider(provider)# Normalize deprecated/alias provider namesifprovidervertexai:providergoogle-vertexelifprovidergoogle:providergoogle-glaifproviderin(openai,openai-chat,openai-responses):from.openaiimportOpenAIProviderreturnOpenAIProviderelifproviderdeepseek:from.deepseekimportDeepSeekProviderreturnDeepSeekProviderelifprovideropenrouter:from.openrouterimportOpenRouterProviderreturnOpenRouterProviderelifprovidervercel:from.vercelimportVercelProviderreturnVercelProviderelifproviderazure:from.azureimportAzureProviderreturnAzureProviderelifproviderin(google-vertex,google-gla):from.googleimportGoogleProviderreturnGoogleProviderelifproviderbedrock:from.bedrockimportBedrockProvider我们再看看具体到provider为moonshotai时代码返回什么锁定到第125行elifprovidermoonshotai:from.moonshotaiimportMoonshotAIProviderreturnMoonshotAIProvider这个时候再查看MoonshotAIProvider的具体实现pydantic_ai.providers.moonshotai.pyclassMoonshotAIProvider(Provider[AsyncOpenAI]):Provider for MoonshotAI platform (Kimi models).propertydefname(self)-str:returnmoonshotaipropertydefbase_url(self)-str:# OpenAI-compatible endpoint, see MoonshotAI docsreturnhttps://api.moonshot.ai/v1propertydefclient(self)-AsyncOpenAI:returnself._client真相大白最终的问题出在这里pydantic-ai把base_url写死了。那如何修复问题呢相信经常用openai的小朋友们这个时候会想到使用openai构造client的时候如果没有传参base_urlopenai就会从环境变量读。如果没有读到就是用使用默认的openai的base_url这个时候查看OpenAIProvider的代码实现pydantic_ai.providers.openai.pyclassOpenAIProvider(Provider[AsyncOpenAI]):Provider for OpenAI API.propertydefname(self)-str:returnopenaipropertydefbase_url(self)-str:returnstr(self.client.base_url)propertydefclient(self)-AsyncOpenAI:returnself._client他的base_url不是写死的那就只要让代码使用OpenAIProvider并提供相应环境变量而不要使用MoonshotAIProvider不就行了现在我们回到解决方案那里给的代码frompydantic_aiimportAgentimportos# pydantic-ai调用openai模型必须配置OPENAI_API_KEY环境变量# 注意配置的是kimi的api-keyassertos.getenv(OPENAI_API_KEY)isnotNone# 可以通过环境变量覆盖掉base_url# 如果不设置这个环境变量他就用的是openai的base_urlassertos.getenv(OPENAI_BASE_URL)https://api.moonshot.cn/v1agentAgent(openai:moonshot-v1-8k,system_prompt请介绍一下pydantic)ansagent.run_sync()print(ans.output)这个代码最后运行时得到的base_url就不是写死的https://api.moonshot.ai/v1/而是我们配置的环境变量了。