Part of #142.
Summary
The primary LLM path in modern Python apps is an agent abstraction, not a direct SDK call. In sapling, every AI feature goes through pydantic-ai: classifier_agent = Agent("google-gla:gemini-2.5-flash", ...) then classifier_agent.run(...) (e.g. routes/documents.py:698, all of agents/*). None are detected — the provider (Gemini) is hidden inside the model string, and agent.run() looks like a local method call.
Root cause
agent = Agent(...) binds the var to package pydantic_ai, which is not in PACKAGE_TO_PROVIDER and is not a registered provider → resolveProvider → dropped at core-scanner.ts:269. There is no concept of "the provider is named in a constructor argument string."
- Irony:
src/scanner/python-waste-detector.ts:18-30 already enumerates langchain*, llama_index*, google.generativeai for N+1 loop detection — that awareness never reaches endpoint detection.
Proposed fix
- Recognize agent/LLM construction for the common frameworks: pydantic-ai
Agent(model, ...); LangChain ChatOpenAI / ChatGoogleGenerativeAI / ChatAnthropic / init_chat_model(...); LlamaIndex LLM ctors.
- Parse the model string → provider:
"google-gla:" / "google-vertex:" → gemini/vertex; "openai:" / bare gpt-* → openai; "anthropic:" / claude-* → anthropic; etc. Bind the agent/LLM var to the resolved provider so the normal resolution path picks up the call.
- Treat
.run() / .run_sync() / .invoke() / .ainvoke() / .complete() / .chat() on a bound agent as a metered call. Note the tool-loop cost multiplier — orchestrators fire N model round-trips per call.
- If the model string is dynamic/unreadable → emit an outbound LLM call of
unknown provider (depends on #D), not nothing.
Evidence
Manual scan of sapling found ~26 Gemini sites, the majority via pydantic-ai agent.run() across agents/document.py, agents/chat_tutor.py, agents/quiz.py, routes/documents.py:698/718/721/725, routes/learn.py:499, routes/notes.py:204/223/252. Extension detected 0.
Acceptance criteria
Part of #142.
Summary
The primary LLM path in modern Python apps is an agent abstraction, not a direct SDK call. In
sapling, every AI feature goes through pydantic-ai:classifier_agent = Agent("google-gla:gemini-2.5-flash", ...)thenclassifier_agent.run(...)(e.g.routes/documents.py:698, all ofagents/*). None are detected — the provider (Gemini) is hidden inside the model string, andagent.run()looks like a local method call.Root cause
agent = Agent(...)binds the var to packagepydantic_ai, which is not inPACKAGE_TO_PROVIDERand is not a registered provider →resolveProvider→ dropped atcore-scanner.ts:269. There is no concept of "the provider is named in a constructor argument string."src/scanner/python-waste-detector.ts:18-30already enumerateslangchain*,llama_index*,google.generativeaifor N+1 loop detection — that awareness never reaches endpoint detection.Proposed fix
Agent(model, ...); LangChainChatOpenAI/ChatGoogleGenerativeAI/ChatAnthropic/init_chat_model(...); LlamaIndex LLM ctors."google-gla:"/"google-vertex:"→ gemini/vertex;"openai:"/ baregpt-*→ openai;"anthropic:"/claude-*→ anthropic; etc. Bind the agent/LLM var to the resolved provider so the normal resolution path picks up the call..run()/.run_sync()/.invoke()/.ainvoke()/.complete()/.chat()on a bound agent as a metered call. Note the tool-loop cost multiplier — orchestrators fire N model round-trips per call.unknownprovider (depends on #D), not nothing.Evidence
Manual scan of
saplingfound ~26 Gemini sites, the majority via pydantic-aiagent.run()acrossagents/document.py,agents/chat_tutor.py,agents/quiz.py,routes/documents.py:698/718/721/725,routes/learn.py:499,routes/notes.py:204/223/252. Extension detected 0.Acceptance criteria
Agent("google-gla:gemini-2.5-flash")+.run()→ providergemini.ChatOpenAI(...).invoke(...)→ provideropenai.