-
Notifications
You must be signed in to change notification settings - Fork 633
[MISC] Fix adapter-ops skill cost-tracking guidance and stale paths #2163
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,7 @@ LiteLLM requires provider prefixes on model names: | |
|
|
||
| 1. **Run initialization script**: | ||
| ```bash | ||
| python .claude/skills/unstract-adapter-extension/scripts/init_llm_adapter.py \ | ||
| python .claude/skills/adapter-ops/scripts/init_llm_adapter.py \ | ||
| --provider newprovider \ | ||
| --name "New Provider" \ | ||
| --description "New Provider LLM adapter" \ | ||
|
|
@@ -122,7 +122,7 @@ LiteLLM requires provider prefixes on model names: | |
|
|
||
| 1. **Run initialization script**: | ||
| ```bash | ||
| python .claude/skills/unstract-adapter-extension/scripts/init_embedding_adapter.py \ | ||
| python .claude/skills/adapter-ops/scripts/init_embedding_adapter.py \ | ||
| --provider newprovider \ | ||
| --name "New Provider" \ | ||
| --description "New Provider embedding adapter" \ | ||
|
|
@@ -190,7 +190,7 @@ LiteLLM requires provider prefixes on model names: | |
|
|
||
| 3. **Run management script** for automated updates: | ||
| ```bash | ||
| python .claude/skills/unstract-adapter-extension/scripts/manage_models.py \ | ||
| python .claude/skills/adapter-ops/scripts/manage_models.py \ | ||
| --adapter llm \ | ||
| --provider openai \ | ||
| --action add \ | ||
|
|
@@ -238,17 +238,17 @@ Compare existing adapter schemas against known LiteLLM features to identify pote | |
| 1. **Run the update checker**: | ||
| ```bash | ||
| # Check all adapters | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py | ||
|
|
||
| # Check specific adapter type | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --adapter llm | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --adapter embedding | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py --adapter llm | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py --adapter embedding | ||
|
|
||
| # Check specific provider | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --provider openai | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py --provider openai | ||
|
|
||
| # Output as JSON | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py --json | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py --json | ||
| ``` | ||
|
|
||
| 2. **Review the report**: | ||
|
|
@@ -278,51 +278,94 @@ Before submitting adapter changes: | |
| - [ ] Adapter class inherits from correct parameter class AND `BaseAdapter` | ||
| - [ ] `get_id()` returns unique `{provider}|{uuid}` format | ||
| - [ ] `get_metadata()` returns dict with `name`, `version`, `adapter`, `description`, `is_active` | ||
| - [ ] **CRITICAL: `get_provider()` returns value matching `litellm_provider` in LiteLLM pricing data** (see below) | ||
| - [ ] `get_provider()` matches the static JSON filename (`static/{get_provider()}.json`) | ||
| - [ ] **CRITICAL: the model string produced by `validate_model()` resolves in LiteLLM's cost map** (see below) | ||
| - [ ] `get_adapter_type()` returns correct `AdapterTypes.LLM` or `AdapterTypes.EMBEDDING` | ||
| - [ ] JSON schema has `adapter_name` as required field | ||
| - [ ] `validate()` method adds correct model prefix | ||
| - [ ] `validate_model()` method handles prefix idempotently (doesn't double-prefix) | ||
| - [ ] All static methods decorated with `@staticmethod` | ||
| - [ ] Icon path follows pattern `/icons/adapter-icons/{Name}.png` | ||
|
|
||
| ### Provider Name Verification (MANDATORY) | ||
| ### Model Prefix Verification (MANDATORY) | ||
|
|
||
| The `get_provider()` return value is used for **cost calculation**. It MUST match the `litellm_provider` field in LiteLLM's pricing data, otherwise costs will show as $0. | ||
| Cost is looked up from the **validated model string**, not from `get_provider()`. The string | ||
| that `validate_model()` produces (e.g. `mistral/mistral-embed`) is passed straight to | ||
| `litellm.cost_per_token()`. If LiteLLM's cost map has no entry for it, the lookup raises, the | ||
| exception is swallowed, and usage records **$0**. | ||
|
|
||
| **Before implementing any adapter, verify the provider name:** | ||
| The lookup sites: | ||
|
|
||
| ```bash | ||
| # Fetch LiteLLM pricing data and check provider name | ||
| curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json | \ | ||
| jq 'to_entries | map(select(.value.litellm_provider != null)) | | ||
| map({key: .key, provider: .value.litellm_provider}) | | ||
| unique_by(.provider) | | ||
| sort_by(.provider) | | ||
| .[].provider' | sort -u | ||
| ``` | ||
| | Path | Site | | ||
| |------|------| | ||
| | LLM | `unstract/sdk1/src/unstract/sdk1/audit.py` — `cost_per_token(model=model_name)` | | ||
| | Embedding | `unstract/sdk1/src/unstract/sdk1/usage_handler.py` — `litellm.cost_per_token(...)` | | ||
|
|
||
| `model_name` is `self._cost_model or self.kwargs["model"]` — the prefixed string. Both call | ||
| sites catch every exception and fall back to `0.0`, so a miss is **silent**. It will not fail a | ||
| test, a build, or a run. The only symptom is revenue-affecting: zero-cost usage rows. | ||
|
|
||
| **Before implementing any adapter, verify the prefix resolves.** Use the pinned LiteLLM in the | ||
| sdk1 venv rather than curling upstream JSON — the pinned version is what actually runs: | ||
|
|
||
| **Common provider name mappings:** | ||
| | Display Name | `get_provider()` Value | LiteLLM Provider | | ||
| |--------------|------------------------|------------------| | ||
| | OpenAI | `openai` | `openai` | | ||
| | Anthropic | `anthropic` | `anthropic` | | ||
| | Azure OpenAI | `azure` | `azure` | | ||
| | Azure AI Foundry | `azure_ai` | `azure_ai` | | ||
| | AWS Bedrock | `bedrock` | `bedrock` | | ||
| | Google VertexAI | `vertex_ai` | `vertex_ai` | | ||
| | Mistral | `mistral` | `mistral` | | ||
| | Ollama | `ollama` | `ollama` | | ||
|
|
||
| **Example verification for Azure AI Foundry:** | ||
| ```bash | ||
| curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json | \ | ||
| jq 'to_entries | map(select(.key | startswith("azure_ai"))) | .[0].value.litellm_provider' | ||
| # Output: "azure_ai" (NOT "azure_ai_foundry") | ||
| cd unstract/sdk1 && uv run python -c " | ||
| import litellm | ||
| from litellm import cost_per_token | ||
|
|
||
| # 1. Does LiteLLM have a native provider for this vendor? | ||
| print([p for p in litellm.provider_list if 'YOUR_VENDOR' in str(p).lower()]) | ||
|
|
||
| # 2. Which of its models are priced? | ||
| print([k for k in litellm.model_cost if k.lower().startswith('YOUR_PROVIDER/')]) | ||
|
|
||
| # 3. Does the string your validate_model() emits actually resolve? | ||
| for m in ['YOUR_PROVIDER/some-model', 'custom_openai/some-model']: | ||
| try: | ||
| print(m, cost_per_token(model=m, prompt_tokens=1_000_000, completion_tokens=1_000_000)) | ||
| except Exception as e: | ||
| print(m, 'RAISES', type(e).__name__, '=> cost silently recorded as 0.0') | ||
| " | ||
| ``` | ||
|
|
||
| **Why this matters:** | ||
| The cost calculation in `platform-service/src/unstract/platform_service/helper/cost_calculation.py` filters models by checking if the provider string is contained in `litellm_provider`. A mismatch (e.g., returning `"azure_ai_foundry"` when LiteLLM uses `"azure_ai"`) causes the cost lookup to fail silently, returning $0. | ||
| **Branded OpenAI-compatible adapters: pick the right base class.** | ||
|
|
||
| Many vendors speak the OpenAI wire protocol, which tempts you to subclass | ||
| `OpenAICompatibleLLMParameters` and just pin `api_base`. But its `validate_model()` | ||
| unconditionally prepends `custom_openai/`, and **nothing** in LiteLLM's cost map is keyed that | ||
| way. That silently forfeits cost tracking. | ||
|
|
||
| | If… | Then | Cost tracking | | ||
| |-----|------|---------------| | ||
| | LiteLLM has a native provider for the vendor | Extend `BaseChatCompletionParameters`, emit `{provider}/{model}` — follow `OpenRouterLLMParameters` | ✅ resolves | | ||
| | LiteLLM has **no** priced models for the vendor | Extend `OpenAICompatibleLLMParameters`, pin `api_base` — follow `NvidiaBuildLLMParameters` | ⚠️ $0 either way, nothing forfeited | | ||
|
|
||
| `NvidiaBuildLLMParameters` is only a safe template because LiteLLM prices zero `nvidia_nim/` | ||
| chat models — there is no cost to lose. Do not copy that shape for a vendor LiteLLM *does* | ||
| price. Check first with the snippet above. | ||
|
|
||
| **What `get_provider()` is actually for:** | ||
|
|
||
| 1. Resolving the static schema path — `{type}1/static/{get_provider()}.json` (case-sensitive `open()`). | ||
| 2. The `provider` column on the usage row (`audit.py`) — metadata only, not used for pricing. | ||
|
|
||
| Keeping it equal to LiteLLM's `litellm_provider` value is still good hygiene, and matters when | ||
| you route natively (the prefix and the provider name coincide). But a correct `get_provider()` | ||
| does **not** on its own guarantee cost resolution — a branded adapter can return `"minimax"`, | ||
| match `litellm_provider` exactly, and still bill $0 because its model string carries the | ||
| `custom_openai/` prefix. | ||
|
|
||
| **Common provider names:** | ||
| | Display Name | `get_provider()` Value | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add the required blank line before the table. The 🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 359-359: Tables should be surrounded by blank lines (MD058, blanks-around-tables) 🪛 SkillSpector (2.3.7)[warning] 74: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified. Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope. (Privilege Escalation (PE2)) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| |--------------|------------------------| | ||
| | OpenAI | `openai` | | ||
| | Anthropic | `anthropic` | | ||
| | Azure OpenAI | `azure` | | ||
| | Azure AI Foundry | `azure_ai` | | ||
| | AWS Bedrock | `bedrock` | | ||
| | Google VertexAI | `vertex_ai` | | ||
| | Mistral | `mistral` | | ||
| | Ollama | `ollama` | | ||
|
|
||
| ## Maintenance Workflow | ||
|
|
||
|
|
@@ -332,7 +375,7 @@ Periodic maintenance to keep adapters current with LiteLLM features: | |
|
|
||
| 1. **Run the update checker**: | ||
| ```bash | ||
| python .claude/skills/unstract-adapter-extension/scripts/check_adapter_updates.py | ||
| python .claude/skills/adapter-ops/scripts/check_adapter_updates.py | ||
| ``` | ||
|
|
||
| 2. **Review LiteLLM changelog** for new provider features: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,14 +148,21 @@ curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_ | |
| ### Why This Matters | ||
|
|
||
| The cost calculation flow: | ||
| 1. `LLM._record_usage()` calls `Audit.push_usage_data()` with provider from `get_provider()` | ||
| 2. Platform service receives usage data with provider name | ||
| 3. `CostCalculationHelper.calculate_cost()` filters models where `provider in litellm_provider` | ||
| 4. If no match found, cost = $0 | ||
|
|
||
| **Example bug**: If `get_provider()` returns `"azure_ai_foundry"` but LiteLLM uses `"azure_ai"`: | ||
| - Check: `"azure_ai_foundry" in "azure_ai"` = `False` | ||
| - Result: Cost calculation returns $0 | ||
| 1. `LLM._record_usage()` logs tokens — no cost math. | ||
| 2. `Audit.push_usage_data()` (LLM) / `UsageHandler` (embedding) calls | ||
| `litellm.cost_per_token(model=model_name)`, where `model_name` is the **prefixed** model | ||
| string emitted by `validate_model()`. | ||
| 3. No cost-map entry for that string → the call raises → the bare `except` records $0. | ||
|
|
||
| The `provider` value from `get_provider()` rides along into the usage row's `provider` column | ||
| but is **not** used for pricing. | ||
|
|
||
| **Example bug**: a branded OpenAI-compatible adapter emits `custom_openai/MiniMax-M3`. LiteLLM | ||
| prices `minimax/MiniMax-M3` but has no `custom_openai/` keys, so cost silently resolves to $0 — | ||
| even though `get_provider()` correctly returns `"minimax"`. | ||
|
|
||
| See `references/adapter_patterns.md` → *Model Prefix Verification* for how to choose a base | ||
| class so the prefix resolves. | ||
|
Comment on lines
+151
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Remove the stale upstream pricing verification. This file still directs readers to curl LiteLLM’s 🤖 Prompt for AI Agents |
||
|
|
||
| ## Models Supporting Advanced Features | ||
|
|
||
|
|
||
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.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make
--jsonoutput valid JSON.check_adapter_updates.pyprintsSDK1 Adapters Path: ...beforejson.dumps(...), so the documented--jsoncommand cannot be parsed byjqor other automation. Send the diagnostic to stderr or suppress it in JSON mode.🧰 Tools
🪛 SkillSpector (2.3.7)
[warning] 74: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope.
(Privilege Escalation (PE2))
🤖 Prompt for AI Agents