Repository navigation
docs+dx: quick-win backlog sweep (#83 #84 #85 #86 #87 #89 #90 #92 #94) - #259
Conversation
Batch of small, self-contained backlog items: - examples: add a shared sample_data.csv and config-driven YAML pipelines for Anthropic (#85) and Groq (#87), both verified to `--dry-run` cleanly with no API key; add a runnable sentiment-analysis-with-costs script using QuickPipeline (#89). sample_data.csv also grounds the existing 10_ollama_local.yaml, which referenced a file that did not exist. - docs: add an Ollama local-inference guide (#92) and a Troubleshooting page covering missing keys, rate limits, context-window, budget, and resume (#94), both wired into SUMMARY.md; add the PyPI downloads badge to the docs landing page matching the README order (#83). - contributing: expand the test section with keyword selection, node ids, unit-only, and integration markers (#90). All new doc snippets pass the docs-name checker; both YAML configs dry-run without a key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#86) - CLI (#84): add return-type annotations to every command and helper in ondine/cli/main.py. Typing the bodies made mypy check them, which surfaced two latent bugs now fixed: a possible None-deref on `specs.output` when a run has no output spec, and `.value` on `specs.llm.provider`, which is `LLMProvider | str` and only has `.value` in the enum case. `mypy ondine/cli/` is clean. - Auth errors (#86): a missing API key surfaced as a bare "Authentication error: 401 ...". It now appends an actionable hint naming the provider's `<PROVIDER>_API_KEY` env var, the `api_key=...` alternative, `ondine list-providers`, and the setup guide. The env var is derived from the spec's provider, so it is correct per provider. Covered by two unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 45 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe pull request expands documentation, adds provider configuration and cost-reporting examples, improves provider-specific authentication errors, and updates CLI return annotations, output handling, and provider display logic. ChangesDocumentation updates
Pipeline examples
Runtime and CLI updates
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ake it Unrelated to the rest of this PR, but it was failing this PR's CI. The scheduling-efficiency check asserted >= 0.70 of the theoretical floor; a loaded runner measured 0.6997 and failed by 0.0003. That threshold is a wall-clock ratio on shared CI, not a correctness boundary — a healthy run measures 70-95%, and the regression it exists to catch (a client that doesn't override ainvoke, so scheduling collapses to serial) runs at ~5%. Lowered to 0.55: still an order of magnitude above the failure mode, with real headroom for runner noise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
tests/unit/test_unified_litellm_minimal.py (1)
521-558: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winExercise the typed LiteLLM path.
Both tests pass a plain
Exception, so they cover only string-based authentication matching. They do not execute the new OpenAI or Anthropic exception branches.Add a
litellm.AuthenticationErrorcase withllm_provider="groq"and assert that the message does not identify Groq as OpenAI. Add one no-key or non-standard credential provider case for_auth_key_hint().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_unified_litellm_minimal.py` around lines 521 - 558, Extend TestAuthErrorGuidance to exercise the typed litellm.AuthenticationError branch by constructing it with llm_provider="groq" and verifying the mapped guidance does not label Groq as OpenAI. Also add a case covering _auth_key_hint() for a provider without a key or with non-standard credentials, asserting the expected fallback behavior.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/guides/providers/ollama.md`:
- Line 55: Update the cost interpolation in the example print statement to
format result.costs.total_cost with exactly two decimal places, preserving the
documented $0.00 output.
In `@docs/guides/troubleshooting.md`:
- Around line 15-18: Update the troubleshooting provider-key example to use the
environment variable matching the selected provider, explicitly showing
Anthropic and Groq alternatives alongside OpenAI. In the checkpoint recovery
example, narrow the broad except Exception handler to the supported recoverable
execution failure, verify both a session and checkpoint exist before using
pipeline.session_id to resume, and re-raise authentication, configuration,
budget, and other unrelated exceptions.
In `@examples/sentiment_analysis_with_costs.py`:
- Around line 60-62: Require OPENAI_API_KEY directly in the guard near lines
60-62 and update the missing-credential message accordingly; update the
description near line 10 and the documented credential requirement near lines
47-48 in examples/sentiment_analysis_with_costs.py to state that this example
uses gpt-4o-mini and requires OPENAI_API_KEY.
In `@ondine/adapters/unified_litellm_client.py`:
- Around line 706-725: Update _auth_key_hint to derive the authentication
guidance from error.llm_provider when available instead of self.provider_name,
so Router-selected deployments are represented correctly. Map known providers to
their credential schemes, including non-<PROVIDER>_API_KEY cases such as Vertex
AI and Ollama, and use a neutral fallback for unknown or keyless providers while
preserving the existing CLI and setup-guide guidance.
- Around line 818-820: Update the authentication-error handling around the
OpenAI exception branch to preserve LiteLLM provider identity: use
error.llm_provider when available for the failure message and _auth_key_hint(),
or exclude litellm.AuthenticationError before the OpenAI check. Add a regression
test constructing litellm.AuthenticationError with llm_provider="groq" and
verify the resulting error identifies Groq rather than OpenAI.
In `@ondine/cli/main.py`:
- Around line 449-452: Update the output success-reporting block guarded by
specs.output to also require specs.output.destination_path, matching the
two-field guard used by resume. Only print the “Output written to” message when
both the output configuration and destination path are present.
---
Nitpick comments:
In `@tests/unit/test_unified_litellm_minimal.py`:
- Around line 521-558: Extend TestAuthErrorGuidance to exercise the typed
litellm.AuthenticationError branch by constructing it with llm_provider="groq"
and verifying the mapped guidance does not label Groq as OpenAI. Also add a case
covering _auth_key_hint() for a provider without a key or with non-standard
credentials, asserting the expected fallback behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: abd4a6b1-399d-4e62-b533-308a0e56ff64
⛔ Files ignored due to path filters (1)
examples/sample_data.csvis excluded by!**/*.csv
📒 Files selected for processing (11)
CONTRIBUTING.mddocs/README.mddocs/SUMMARY.mddocs/guides/providers/ollama.mddocs/guides/troubleshooting.mdexamples/claude_config.yamlexamples/groq_config.yamlexamples/sentiment_analysis_with_costs.pyondine/adapters/unified_litellm_client.pyondine/cli/main.pytests/unit/test_unified_litellm_minimal.py
|
|
||
| result = pipeline.execute() | ||
| print(result.to_pandas()) | ||
| print(f"Cost: ${result.costs.total_cost}") # $0.00 — it ran on your machine |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Format the cost value to match the documented output.
The expression does not specify decimal precision. The example can print $0.0 instead of $0.00. Format the value with two decimal places.
Proposed fix
-print(f"Cost: ${result.costs.total_cost}") # $0.00 — it ran on your machine
+print(f"Cost: ${result.costs.total_cost:.2f}") # $0.00 — it ran on your machine📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| print(f"Cost: ${result.costs.total_cost}") # $0.00 — it ran on your machine | |
| print(f"Cost: ${result.costs.total_cost:.2f}") # $0.00 — it ran on your machine |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/guides/providers/ollama.md` at line 55, Update the cost interpolation in
the example print statement to format result.costs.total_cost with exactly two
decimal places, preserving the documented $0.00 output.
| **Fix:** set the key for your provider, then re-run: | ||
|
|
||
| ```bash | ||
| export OPENAI_API_KEY="sk-..." # pragma: allowlist secret |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Use the environment variable for the selected provider.
The text lists ANTHROPIC_API_KEY and GROQ_API_KEY, but the copy-paste command exports only OPENAI_API_KEY. Users of other providers can follow this command and still receive a missing-key error. Add provider-specific examples or state that the variable must be replaced for the selected provider.
Proposed fix
-**Fix:** set the key for your provider, then re-run:
+**Fix:** set the environment variable for your provider, then re-run. For OpenAI:
export OPENAI_API_KEY="sk-..." # pragma: allowlist secret+For Anthropic, use ANTHROPIC_API_KEY. For Groq, use GROQ_API_KEY.
</details>
</review_comment>
<review_comment line_ranges="106-110">
**Do not resume after every exception.**
`except Exception` also catches authentication, configuration, and budget errors. The example then resumes with `pipeline.session_id`, even when no checkpointable session exists. Catch only the recoverable execution failure supported by checkpoint recovery, and verify that a session and checkpoint exist before resuming. Re-raise other exceptions.
</review_comment>
<review_comment line_ranges="1-14,19-105,111-122">
LGTM!
</review_comment>
</file_review>
<consolidated_comments>
none
</consolidated_comments>
</review_response>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
**Fix:** set the environment variable for your provider, then re-run. For OpenAI:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/guides/troubleshooting.md` around lines 15 - 18, Update the
troubleshooting provider-key example to use the environment variable matching
the selected provider, explicitly showing Anthropic and Groq alternatives
alongside OpenAI. In the checkpoint recovery example, narrow the broad except
Exception handler to the supported recoverable execution failure, verify both a
session and checkpoint exist before using pipeline.session_id to resume, and
re-raise authentication, configuration, budget, and other unrelated exceptions.
| if not any(key.endswith("_API_KEY") and os.environ[key] for key in os.environ): | ||
| print("No *_API_KEY set — set one (e.g. OPENAI_API_KEY) to run for real.") | ||
| return |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require the API key for the selected provider.
gpt-4o-mini auto-selects OpenAI. An Anthropic, Groq, or unrelated *_API_KEY passes the current guard, but execution then fails because OPENAI_API_KEY is absent.
examples/sentiment_analysis_with_costs.py#L60-L62: CheckOPENAI_API_KEYdirectly and update the error text.examples/sentiment_analysis_with_costs.py#L10-L10: State that this version of the example requiresOPENAI_API_KEY.examples/sentiment_analysis_with_costs.py#L47-L48: Keep the documented credential requirement aligned with the selected OpenAI model.
Proposed fix
- export OPENAI_API_KEY="..." # or any provider's key
+ export OPENAI_API_KEY="..."
@@
- if not any(key.endswith("_API_KEY") and os.environ[key] for key in os.environ):
- print("No *_API_KEY set — set one (e.g. OPENAI_API_KEY) to run for real.")
+ if not os.environ.get("OPENAI_API_KEY"):
+ print("No OPENAI_API_KEY set — set it to run for real.")
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if not any(key.endswith("_API_KEY") and os.environ[key] for key in os.environ): | |
| print("No *_API_KEY set — set one (e.g. OPENAI_API_KEY) to run for real.") | |
| return | |
| if not os.environ.get("OPENAI_API_KEY"): | |
| print("No OPENAI_API_KEY set — set it to run for real.") | |
| return |
📍 Affects 1 file
examples/sentiment_analysis_with_costs.py#L60-L62(this comment)examples/sentiment_analysis_with_costs.py#L10-L10examples/sentiment_analysis_with_costs.py#L47-L48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/sentiment_analysis_with_costs.py` around lines 60 - 62, Require
OPENAI_API_KEY directly in the guard near lines 60-62 and update the
missing-credential message accordingly; update the description near line 10 and
the documented credential requirement near lines 47-48 in
examples/sentiment_analysis_with_costs.py to state that this example uses
gpt-4o-mini and requires OPENAI_API_KEY.
| def _auth_key_hint(self) -> str: | ||
| """Actionable next step to append to an authentication failure. | ||
|
|
||
| A bare "authentication failed" tells the user *that* something is wrong | ||
| but not *what to do*. The most common cause is a missing API key, so we | ||
| name the environment variable it is read from (by convention | ||
| ``<PROVIDER>_API_KEY``) and point at the CLI and setup guide. Returned | ||
| as a trailing fragment so it slots onto any of the auth messages below. | ||
| """ | ||
| provider = (self.provider_name or "").strip() | ||
| if provider: | ||
| env_var = f"{provider.upper()}_API_KEY" | ||
| how = f"set {env_var} (e.g. `export {env_var}=...`)" | ||
| else: | ||
| how = "set the provider's API key environment variable" | ||
| return ( | ||
| f" — {how}, or pass api_key=... to .with_llm(). " | ||
| "Run `ondine list-providers` to see providers, or the setup guide: " | ||
| "https://docs.ondine.dev/getting-started/installation." | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- file outline ---'
ast-grep outline ondine/adapters/unified_litellm_client.py
printf '%s\n' '--- relevant symbols and references ---'
rg -n -C 5 '_auth_key_hint|_init_router|provider_name|llm_provider|authentication|Authentication|Router' ondine/adapters/unified_litellm_client.py
printf '%s\n' '--- provider and router-related files ---'
rg -n -C 3 'Router\(|llm_provider|api_key|provider_name|model=' ondine -g '*.py' | head -n 300Repository: ptimizeroracle/ondine
Length of output: 37405
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- client initialization and error mapping ---'
sed -n '266,375p' ondine/adapters/unified_litellm_client.py
sed -n '553,845p' ondine/adapters/unified_litellm_client.py
printf '%s\n' '--- router configuration examples and tests ---'
rg -n -C 6 'router_config|model_list|llm_provider|custom_llm_provider|Router' . -g '*.py' -g '*.yaml' -g '*.yml' -g '*.json' | head -n 500
printf '%s\n' '--- dependency versions ---'
rg -n -C 2 'litellm|instructor' pyproject.toml poetry.lock uv.lock requirements*.txt setup.cfg setup.py 2>/dev/null || trueRepository: ptimizeroracle/ondine
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- router builder implementation ---'
rg -n -C 12 'def with_router|router_config|model_list' ondine -g '*.py' | head -n 220
printf '%s\n' '--- concrete multi-provider router examples ---'
sed -n '100,165p' scripts/benchmark_router.py
sed -n '25,45p' tests/unit/test_resilience.py
printf '%s\n' '--- provider enum and custom-provider handling ---'
rg -n -C 8 'class LLMProvider|OPENAI_COMPATIBLE|provider_name' ondine/core/specifications.py | head -n 260Repository: ptimizeroracle/ondine
Length of output: 25973
🌐 Web query:
LiteLLM exception llm_provider attribute Router authentication error provider environment variable documentation Vertex AI Ollama API key
💡 Result:
Authentication errors in the LiteLLM Router generally stem from misconfigured credentials, missing provider identifiers, or incorrect API key passing. The llm_provider attribute is a critical component for LiteLLM to correctly identify, route, and authenticate requests [1][2][3][4]. Key troubleshooting points based on common LiteLLM Router behavior: 1. Provider Identification: If you receive a "LLM Provider NOT provided" or "Unmapped LLM provider" error, the Router failed to map your requested model to a supported provider [2][3]. Ensure the model name follows the required naming convention (e.g., provider/model-name) or that the deployment configuration explicitly defines the provider [2][5][6]. 2. Authentication Error Diagnosis: - Provider Errors: If the error message includes an exception name like VertexAIException or OpenAIException, it indicates the issue is with the external provider, typically caused by an invalid or missing API key [7][8]. - Gateway Errors: Errors without a provider name indicate the issue is with the LiteLLM AI Gateway itself, such as an invalid virtual master key [7]. 3. Vertex AI Specifics: - Authentication Method: Vertex AI often defaults to Application Default Credentials (ADC) [5][9][10]. If your setup is failing with "Invalid API Key" or "DefaultCredentialsError," verify if your environment is correctly providing the required credentials (e.g., GOOGLE_APPLICATION_CREDENTIALS path to a service account JSON, or Workload Identity) [11][9][10]. - Gemini/Vertex Confusion: If you intend to use a Google AI Studio API key for Gemini, explicitly prepend the model with gemini/ in your configuration to avoid LiteLLM defaulting to the Vertex AI endpoint [5]. 4. Ollama Configuration: For local providers like Ollama, no API key is typically required [8][10]. If you are encountering an authentication error while using Ollama, ensure you have set api_key: "" or left it blank in your litellm_params to prevent the Router from attempting an unnecessary authentication handshake [8][10]. 5. Environment Variable Convention: LiteLLM reads credentials from standard environment variables (e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY) at startup [8]. If running the proxy, ensure these variables are exported in the environment where the proxy process is running [8]. For more granular control, use a config.yaml to specify per-model keys [8][6].
Citations:
- 1: https://github.com/BerriAI/litellm/blob/main/litellm/exceptions.py
- 2: [Bug]: Wildcard model_list with google/* (and openrouter) produces errors BerriAI/litellm#14595
- 3: [Bug]: v1.83.14 auto_router/complexity_router in litellm_params throws "Unmapped LLM provider" 400 error BerriAI/litellm#27473
- 4: https://docs.litellm.ai/docs/provider_registration/
- 5: [Bug]: LiteLLM does not authenticate Gemini with API key. Asking for Vertex credentials. BerriAI/litellm#14771
- 6: https://docs.litellm.ai/docs/routing
- 7: https://docs.litellm.ai/docs/proxy/error_diagnosis
- 8: https://markaicode.com/errors/litellm-authentication-failed-fix/
- 9: [Bug]: Can't authenticate to vertex ai image generation models through v1/images/edits BerriAI/litellm#21691
- 10: https://docs.emergence.ai/platform/llm-gateway
- 11: [Bug]: vertex_credentials Not Being Used for Authentication in Vertex AI Calls (Error 400: Invalid API Key) BerriAI/litellm#12940
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import ast
path = Path("ondine/adapters/unified_litellm_client.py")
tree = ast.parse(path.read_text())
init = next(
n for n in ast.walk(tree)
if isinstance(n, ast.FunctionDef) and n.name == "__init__"
)
hint = next(
n for n in ast.walk(tree)
if isinstance(n, ast.FunctionDef) and n.name == "_auth_key_hint"
)
# Reproduce only the provider/model normalization visible in __init__.
def normalized_provider(spec_provider, model):
provider = str(spec_provider).replace("litellm_", "").replace("litellm", "").strip()
if provider == "openai_compatible":
model = model if model.startswith("openai/") else f"openai/{model}"
provider = "openai"
elif "/" not in model and provider:
model = f"{provider}/{model}"
provider_name = (
(provider if provider else model.split("/", 1)[0])
if "/" in model
else provider
)
return model, provider_name
cases = [
("openai_compatible", "llama3.1:8b"),
("openai_compatible", "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"),
("openai", "gpt-4o-mini"),
]
for provider, model in cases:
normalized_model, stored_provider = normalized_provider(provider, model)
env_var = f"{stored_provider.upper()}_API_KEY" if stored_provider else None
print({
"input_provider": provider,
"input_model": model,
"stored_model": normalized_model,
"stored_provider_name": stored_provider,
"hinted_env_var": env_var,
})
# Show that the Router call receives a shared model name while deployment
# providers remain in each deployment's litellm_params.
router_config = {
"model_list": [
{"model_name": "classifier", "litellm_params": {"model": "groq/llama-3.3-70b"}},
{"model_name": "classifier", "litellm_params": {"model": "openai/gpt-4o-mini"}},
]
}
shared_model = {deployment["model_name"] for deployment in router_config["model_list"]}.pop()
deployment_models = [
deployment["litellm_params"]["model"]
for deployment in router_config["model_list"]
]
print({
"router_shared_model": shared_model,
"deployment_models": deployment_models,
"deployment_providers": [model.split("/", 1)[0] for model in deployment_models],
})
PY
printf '%s\n' '--- LiteLLM exception fields in the pinned/source dependency metadata ---'
curl -L --fail --silent --show-error \
https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/exceptions.py |
rg -n -C 6 'class (AuthenticationError|APIError)|llm_provider|model:' | head -n 220Repository: ptimizeroracle/ondine
Length of output: 9259
Base the authentication hint on the failing deployment.
self.provider_name stores the initial provider and remains unchanged when Router selects a deployment. A router may select different providers, so the hint can name the wrong environment variable. The <PROVIDER>_API_KEY convention also fails for providers such as Vertex AI and Ollama.
Use error.llm_provider when available. Map known providers to their credential schemes and use a neutral fallback for unknown or keyless providers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ondine/adapters/unified_litellm_client.py` around lines 706 - 725, Update
_auth_key_hint to derive the authentication guidance from error.llm_provider
when available instead of self.provider_name, so Router-selected deployments are
represented correctly. Map known providers to their credential schemes,
including non-<PROVIDER>_API_KEY cases such as Vertex AI and Ollama, and use a
neutral fallback for unknown or keyless providers while preserving the existing
CLI and setup-guide guidance.
Source: MCP tools
| return InvalidAPIKeyError( | ||
| f"OpenAI authentication failed: {error}{self._auth_key_hint()}" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file=$(git ls-files | grep -F 'ondine/adapters/unified_litellm_client.py' | head -n1)
printf 'FILE=%s\n' "$file"
wc -l "$file"
rg -n -C 12 '_auth_key_hint|AuthenticationError|OpenAI authentication failed|provider_name|Router|router' "$file"Repository: ptimizeroracle/ondine
Length of output: 28829
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- imports and error mapper ---'
sed -n '1,80p;727,845p' ondine/adapters/unified_litellm_client.py
printf '%s\n' '--- provider/spec definitions and tests ---'
rg -n -C 8 'class LLMSpec|provider:|UnifiedLiteLLMClient|_map_provider_error|InvalidAPIKeyError|litellm' ondine tests 2>/dev/null | head -n 260
printf '%s\n' '--- dependency pins ---'
rg -n -i -C 2 'litellm|openai|anthropic' pyproject.toml poetry.lock requirements*.txt uv.lock setup.cfg setup.py 2>/dev/null || trueRepository: ptimizeroracle/ondine
Length of output: 37192
🌐 Web query:
LiteLLM v1.91.1 exceptions.py AuthenticationError llm_provider OpenAI AuthenticationError subclass
💡 Result:
In LiteLLM, the AuthenticationError class in litellm/exceptions.py is a direct subclass of openai.AuthenticationError [1][2][3]. When raised, the exception is initialized with several attributes that provide context, including the llm_provider, model, and optional information such as litellm_debug_info, max_retries, and num_retries [1][4]. While it inherits from the OpenAI exception, it overrides the init method to handle these custom LiteLLM-specific fields and sets a status_code of 401 [1][3]. It also ensures compatibility with internal LiteLLM error handling by accepting an optional httpx.Response object [1][5]. This structure is consistent across recent versions of LiteLLM, including v1.91.1, as part of the library's unified exception mapping system [1][6]. The primary purpose of this subclassing is to allow LiteLLM to surface standardized error information while maintaining compatibility with the OpenAI-style error handling that many developers expect when using the library [6][7].
Citations:
- 1: https://github.com/BerriAI/litellm/blob/main/litellm/exceptions.py
- 2: https://github.com/BerriAI/litellm/blob/62920a0c/litellm/exceptions.py
- 3: https://github.com/BerriAI/litellm/blob/e15b37a1/litellm/exceptions.py
- 4: https://github.com/BerriAI/litellm/blob/4dc9726d/litellm/exceptions.py
- 5: https://github.com/BerriAI/litellm/blob/84c1414a/litellm/exceptions.py
- 6: https://docs.litellm.ai/docs/exception_mapping
- 7: https://github.com/BerriAI/litellm
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import ast
import urllib.request
for version in ("v1.91.1", "v1.94.0"):
url = f"https://raw.githubusercontent.com/BerriAI/litellm/{version}/litellm/exceptions.py"
text = urllib.request.urlopen(url, timeout=10).read().decode()
tree = ast.parse(text)
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "AuthenticationError")
bases = [ast.unparse(b) for b in cls.bases]
init = next((n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == "__init__"), None)
args = [a.arg for a in init.args.args] if init else []
attrs = sorted({
n.targets[0].attr
for n in ast.walk(init)
if isinstance(n, ast.Assign)
and len(n.targets) == 1
and isinstance(n.targets[0], ast.Attribute)
and isinstance(n.targets[0].value, ast.Name)
and n.targets[0].value.id == "self"
}) if init else []
print(version, "bases=", bases, "init_args=", args, "self_attrs=", attrs)
for i, line in enumerate(text.splitlines(), 1):
if "class AuthenticationError" in line or "llm_provider" in line:
print(f" {i}: {line.strip()}")
PY
printf '%s\n' '--- local mapper test coverage ---'
rg -n -C 8 'map_provider_error|OpenAI authentication failed|Anthropic authentication failed|InvalidAPIKeyError' tests ondine | head -n 240Repository: ptimizeroracle/ondine
Length of output: 2445
🏁 Script executed:
#!/bin/bash
set -e
for version in v1.91.1 v1.94.0; do
url="https://raw.githubusercontent.com/BerriAI/litellm/$version/litellm/exceptions.py"
curl -fsSLk "$url" | python3 -c '
import ast, sys
text = sys.stdin.read()
tree = ast.parse(text)
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "AuthenticationError")
print("bases:", [ast.unparse(b) for b in cls.bases])
init = next((n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == "__init__"), None)
print("init_args:", [a.arg for a in init.args.args] if init else [])
print("llm_provider_mentions:")
for i, line in enumerate(text.splitlines(), 1):
if "llm_provider" in line or "class AuthenticationError" in line:
print(f"{i}: {line.strip()}")
'
done
printf '%s\n' '--- local mapper test coverage ---'
rg -n -C 8 'map_provider_error|OpenAI authentication failed|Anthropic authentication failed|InvalidAPIKeyError' tests ondine || trueRepository: ptimizeroracle/ondine
Length of output: 39018
Preserve the LiteLLM provider in authentication errors.
litellm.AuthenticationError subclasses openai.AuthenticationError, so Groq and other providers enter this branch and produce "OpenAI authentication failed". Use error.llm_provider when available for the message and key hint, or exclude LiteLLM's exception before the OpenAI check. Add a regression test with litellm.AuthenticationError(llm_provider="groq", ...).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ondine/adapters/unified_litellm_client.py` around lines 818 - 820, Update the
authentication-error handling around the OpenAI exception branch to preserve
LiteLLM provider identity: use error.llm_provider when available for the failure
message and _auth_key_hint(), or exclude litellm.AuthenticationError before the
OpenAI check. Add a regression test constructing litellm.AuthenticationError
with llm_provider="groq" and verify the resulting error identifies Groq rather
than OpenAI.
Source: MCP tools
| if specs.output: | ||
| console.print( | ||
| f"\n[green]Output written to: {specs.output.destination_path}[/green]" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- relevant definitions and uses ---'
rg -n -C 8 'class PipelineSpecifications|PipelineSpecifications|destination_path|def process|def resume|specs\.output' ondine/cli/main.py ondine
printf '%s\n' '--- focused source ranges ---'
sed -n '420,465p' ondine/cli/main.py
sed -n '700,740p' ondine/cli/main.pyRepository: ptimizeroracle/ondine
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(main\.py|.*(spec|pipeline|output).*(py|yaml|toml))$' | head -200
printf '%s\n' '--- all relevant references ---'
rg -n -C 5 'destination_path|PipelineSpecifications|class .*Output|output\s*=' ondine
printf '%s\n' '--- static AST facts for main.py ---'
python3 - <<'PY'
import ast
from pathlib import Path
p = Path("ondine/cli/main.py")
tree = ast.parse(p.read_text())
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in {"process", "resume"}:
print(f"{node.name}: lines {node.lineno}-{node.end_lineno}")
for child in ast.walk(node):
if isinstance(child, ast.If):
text = ast.get_source_segment(p.read_text(), child.test)
if text and ("specs.output" in text or "destination_path" in text):
print(f" if line {child.lineno}: {text}")
PYRepository: ptimizeroracle/ondine
Length of output: 42716
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import ast
path = Path("ondine/cli/main.py")
source = path.read_text()
tree = ast.parse(source)
def function(name):
return next(
n for n in tree.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == name
)
for name in ("process", "resume"):
fn = function(name)
print(f"{name}:")
for node in ast.walk(fn):
if isinstance(node, ast.If):
test = ast.unparse(node.test)
if "output" in test or "destination_path" in test:
print(f" line {node.lineno}: if {test}")
# Extract the output-message expression without importing or running repository code.
for node in ast.walk(tree):
if isinstance(node, ast.JoinedStr) and "Output written to:" in ast.get_source_segment(source, node):
print("message:", ast.get_source_segment(source, node).strip())
PYRepository: ptimizeroracle/ondine
Length of output: 482
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from dataclasses import dataclass
`@dataclass`
class OutputSpec:
destination_path: object = None
`@dataclass`
class Specs:
output: OutputSpec | None = None
specs = Specs(output=OutputSpec())
current_guard = bool(specs.output)
proposed_guard = bool(specs.output and specs.output.destination_path)
message = f"Output written to: {specs.output.destination_path}"
print(f"output_present={bool(specs.output)}")
print(f"destination_path_present={bool(specs.output.destination_path)}")
print(f"current_guard={current_guard}")
print(f"proposed_guard={proposed_guard}")
print(f"current_message={message!r}")
assert current_guard is True
assert proposed_guard is False
assert message.endswith("None")
PYRepository: ptimizeroracle/ondine
Length of output: 335
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from dataclasses import dataclass
from typing import Optional
`@dataclass`
class OutputSpec:
destination_path: object = None
`@dataclass`
class Specs:
output: Optional[OutputSpec] = None
specs = Specs(output=OutputSpec())
current_guard = bool(specs.output)
proposed_guard = bool(specs.output and specs.output.destination_path)
message = f"Output written to: {specs.output.destination_path}"
print(f"output_present={bool(specs.output)}")
print(f"destination_path_present={bool(specs.output.destination_path)}")
print(f"current_guard={current_guard}")
print(f"proposed_guard={proposed_guard}")
print(f"current_message={message!r}")
assert current_guard is True
assert proposed_guard is False
assert message.endswith("None")
PYRepository: ptimizeroracle/ondine
Length of output: 292
Guard destination_path before reporting success.
If specs.output.destination_path is None, the current guard prints Output written to: None. Match the two-field guard used by resume.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ondine/cli/main.py` around lines 449 - 452, Update the output
success-reporting block guarded by specs.output to also require
specs.output.destination_path, matching the two-field guard used by resume. Only
print the “Output written to” message when both the output configuration and
destination path are present.
What
A batch of small, self-contained backlog items — docs, examples, and two focused code changes. Grouped into two commits (docs/examples, then production code) so review is easy.
Closes
docs/README.md, matching README orderondine/cli/main.py; surfaced + fixed 2 latent bugs (see below)examples/claude_config.yaml— dry-runs cleanly, no key<PROVIDER>_API_KEY+list-providers+ setup guide; 2 unit testsexamples/groq_config.yaml— dry-runs cleanly, no keyexamples/sentiment_analysis_with_costs.py(QuickPipeline)docs/guides/providers/ollama.md+ navdocs/guides/troubleshooting.md+ nav — 6 common issuesTwo latent bugs found by typing the CLI (#84)
Adding return annotations made mypy check the command bodies, which exposed:
specs.output(it'sOutputSpec | None) when a run has no output spec;.valueonspecs.llm.provider, which isLLMProvider | str—.valueonly exists in the enum case.Both fixed.
mypy ondine/cli/is clean.Verification
ondine process --config … --dry-runcleanly with no API key.#86red-phase proven: the pre-change message lacksOPENAI_API_KEY, the new one includes it.examples/sample_data.csvwas force-added (repo ignores*.csv); it also grounds the existing10_ollama_local.yaml, which referenced a file that didn't exist.ai assistance: i directed this work with help from claude code.
Summary by CodeRabbit
Documentation
New Features
Bug Fixes