Skip to content

Commit c0e18c0

Browse files
kevinelliottclaude
andcommitted
Add provider pricing integration from Catwalk (v0.5.0)
Provider Pricing System: - New `agentpipe providers` command with list/show/update subcommands - Integrated with Catwalk's provider configs for accurate pricing - Support for 16 AI providers with 100+ models - Smart model matching (exact/prefix/fuzzy) with comprehensive logging - Hybrid config loading: embedded defaults + optional ~/.agentpipe/providers.json override Cost Estimation Improvements: - Refactored EstimateCost() to use provider registry instead of hardcoded prices - Falls back to $0 with warning for unknown models - Detailed debug logging with model and provider information - Legacy function preserved as EstimateCostLegacy() for compatibility Provider Data: - Single consolidated JSON file with all pricing info (120KB embedded) - Auto-generated from Catwalk's 16 provider configs via build script - Includes model pricing, context windows, capabilities, and more - Version-tracked with update timestamps and source attribution Technical Implementation: - New internal/providers/ package with complete provider management - Provider registry with RWMutex for thread-safe concurrent access - HTTP client with retry logic and exponential backoff for Catwalk fetches - Build script (scripts/update-providers.go) to regenerate providers.json - go:embed directive for zero-dependency embedded pricing data - Supports both JSON and human-readable table output formats Testing & Quality: - Comprehensive test coverage (>80%) for all provider functionality - Registry tests for exact/prefix/fuzzy matching, reload, etc. - Fetcher tests for GitHub API integration - Cost estimation tests with real pricing data - All linting checks pass, all tests pass with race detector Documentation: - Updated CHANGELOG.md with v0.5.0 release notes - Added detailed providers command section to README.md - Updated CLAUDE.md with recent changes Benefits: - Accurate costs from Catwalk, not hardcoded approximations - Simple `agentpipe providers update` to get latest pricing - 16 providers with 100+ models covered - Smart matching handles model ID variations automatically - Better conversation cost tracking insights - Backwards compatible with legacy pricing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0a6337d commit c0e18c0

File tree

13 files changed

+4909
-21
lines changed

13 files changed

+4909
-21
lines changed

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v0.5.0] - 2025-10-25
11+
12+
### Added
13+
- **Provider Pricing Integration from Catwalk**
14+
- Accurate cost estimation using [Catwalk](https://github.com/charmbracelet/catwalk) provider configs
15+
- Support for 16 AI providers with comprehensive pricing data:
16+
- AIHubMix, Anthropic, Azure OpenAI, AWS Bedrock, Cerebras, Chutes
17+
- DeepSeek, Gemini, Groq, Hugging Face, OpenAI, OpenRouter
18+
- Venice, Vertex AI, xAI, and more
19+
- New `agentpipe providers` command with subcommands:
20+
- `list` - Show all providers and models with pricing
21+
- `show <provider>` - Display detailed provider information
22+
- `update` - Fetch latest pricing from Catwalk GitHub
23+
- Smart model matching with exact, prefix, and fuzzy matching
24+
- Hybrid config loading: embedded defaults with optional `~/.agentpipe/providers.json` override
25+
- Comprehensive test coverage (>80%) for all provider functionality
26+
27+
### Changed
28+
- **Cost Estimation**: Refactored `EstimateCost()` to use provider registry instead of hardcoded prices
29+
- Falls back to $0 with warning for unknown models
30+
- Provides detailed debug logging with model and provider information
31+
- Legacy function preserved as `EstimateCostLegacy()` for compatibility
32+
- **Provider Data**: Single consolidated JSON file with all pricing info (120KB embedded)
33+
- Auto-generated from Catwalk's 16 provider configs
34+
- Includes model pricing, context windows, capabilities, and more
35+
- Version-tracked with update timestamps and source attribution
36+
37+
### Technical Details
38+
- New `internal/providers/` package with complete provider management
39+
- Provider registry with RWMutex for thread-safe concurrent access
40+
- HTTP client with retry logic and exponential backoff for Catwalk fetches
41+
- Build script (`scripts/update-providers.go`) to regenerate providers.json
42+
- go:embed directive for zero-dependency embedded pricing data
43+
- Supports both JSON and human-readable table output formats
44+
45+
### Benefits
46+
-**Accurate Costs**: Real pricing from Catwalk, not hardcoded approximations
47+
- 🔄 **Always Current**: Simple `agentpipe providers update` to get latest pricing
48+
- 🌍 **Comprehensive**: 16 providers with 100+ models covered
49+
- 🎯 **Smart Matching**: Handles model ID variations automatically
50+
- 📊 **Better Insights**: More accurate conversation cost tracking
51+
- 🛡️ **Backwards Compatible**: Legacy pricing still available if needed
52+
1053
## [v0.4.9] - 2025-10-25
1154

1255
### Added

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ goimports -local github.com/kevinelliott/agentpipe -w .
167167
```
168168

169169
## Recent Changes Log
170+
- **v0.5.0 (2025-10-25)**: Provider Pricing Integration
171+
- Added `agentpipe providers` command with list/show/update subcommands
172+
- Integrated with Catwalk's provider configs for accurate pricing
173+
- Support for 16 AI providers with 100+ models
174+
- Smart model matching (exact/prefix/fuzzy) with comprehensive logging
175+
- Hybrid config loading: embedded defaults + optional override
176+
- Refactored `EstimateCost()` to use provider registry
177+
- Build script to regenerate providers.json from Catwalk
178+
- >80% test coverage for providers package
179+
- 120KB embedded JSON with all pricing data (go:embed)
170180
- **v0.3.0 (2025-10-21)**: Streaming Bridge feature
171181
- Added opt-in real-time conversation streaming to AgentPipe Web
172182
- Created `internal/bridge/` package with comprehensive infrastructure

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,67 @@ Use this command to:
519519
- Troubleshoot missing dependencies
520520
- Validate authentication status before starting conversations
521521

522+
### `agentpipe providers`
523+
524+
Manage AI provider configurations and pricing data.
525+
526+
**Subcommands:**
527+
- `list` - List all available providers and models with pricing
528+
- `show <provider>` - Show detailed information for a specific provider
529+
- `update` - Update provider pricing data from Catwalk
530+
531+
**Flags:**
532+
- `--json` - Output in JSON format
533+
- `-v, --verbose` - Show detailed model information (list command only)
534+
535+
**Examples:**
536+
```bash
537+
# List all providers
538+
agentpipe providers list
539+
540+
# List providers with detailed model info
541+
agentpipe providers list --verbose
542+
543+
# Show Anthropic provider details
544+
agentpipe providers show anthropic
545+
546+
# Get provider data as JSON
547+
agentpipe providers show openai --json
548+
549+
# Update pricing from Catwalk
550+
agentpipe providers update
551+
```
552+
553+
**Features:**
554+
- **Accurate Pricing**: Uses real pricing data from [Catwalk](https://github.com/charmbracelet/catwalk)
555+
- **16 Providers**: AIHubMix, Anthropic, Azure OpenAI, AWS Bedrock, Cerebras, Chutes, DeepSeek, Gemini, Groq, Hugging Face, OpenAI, OpenRouter, Venice, Vertex AI, xAI, and more
556+
- **Smart Matching**: Automatically matches model names with exact, prefix, or fuzzy matching
557+
- **Always Current**: Simple `agentpipe providers update` fetches latest pricing from Catwalk GitHub
558+
- **Hybrid Loading**: Uses embedded defaults but allows local override via `~/.agentpipe/providers.json`
559+
560+
**Output includes:**
561+
- Model IDs and display names
562+
- Input/output pricing per 1M tokens
563+
- Context window sizes
564+
- Reasoning capabilities
565+
- Attachment support
566+
567+
**Example Output:**
568+
```
569+
Provider Pricing Data (v1.0)
570+
Updated: 2025-10-25T21:38:20Z
571+
Source: https://github.com/charmbracelet/catwalk
572+
573+
PROVIDER ID MODELS DEFAULT LARGE DEFAULT SMALL
574+
-------- -- ------ ------------- -------------
575+
AIHubMix aihubmix 11 claude-sonnet-4-5 claude-3-5-haiku
576+
Anthropic anthropic 9 claude-sonnet-4-5-20250929 claude-3-5-haiku-20241022
577+
Azure OpenAI azure 14 gpt-5 gpt-5-mini
578+
DeepSeek deepseek 2 deepseek-reasoner deepseek-chat
579+
Gemini gemini 2 gemini-2.5-pro gemini-2.5-flash
580+
OpenAI openai 14 gpt-5 gpt-5-mini
581+
```
582+
522583
### `agentpipe agents`
523584

524585
Manage AI agent CLI installations with version checking and upgrade capabilities.

cmd/providers.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"text/tabwriter"
9+
10+
"github.com/spf13/cobra"
11+
12+
"github.com/kevinelliott/agentpipe/internal/providers"
13+
"github.com/kevinelliott/agentpipe/pkg/log"
14+
)
15+
16+
var (
17+
providersJSONOutput bool
18+
providersVerbose bool
19+
)
20+
21+
var providersCmd = &cobra.Command{
22+
Use: "providers",
23+
Short: "Manage AI provider configurations and pricing",
24+
Long: `Manage AI provider configurations and pricing data.
25+
26+
Provider pricing data is sourced from Catwalk's provider configs and can be
27+
updated to get the latest pricing information.`,
28+
}
29+
30+
var providersListCmd = &cobra.Command{
31+
Use: "list",
32+
Short: "List all available providers and models",
33+
Long: `List all available AI providers and their models with pricing information.
34+
35+
By default, displays a human-readable table. Use --json for JSON output.`,
36+
Run: runProvidersList,
37+
}
38+
39+
var providersShowCmd = &cobra.Command{
40+
Use: "show <provider-id>",
41+
Short: "Show detailed information for a specific provider",
42+
Long: `Show detailed information for a specific provider, including all available
43+
models and their pricing.
44+
45+
Example:
46+
agentpipe providers show anthropic
47+
agentpipe providers show openai --json`,
48+
Args: cobra.ExactArgs(1),
49+
Run: runProvidersShow,
50+
}
51+
52+
var providersUpdateCmd = &cobra.Command{
53+
Use: "update",
54+
Short: "Update provider pricing data from Catwalk",
55+
Long: `Update provider pricing data by fetching the latest configurations from
56+
Catwalk's GitHub repository.
57+
58+
This will download all provider configs and save them to:
59+
~/.agentpipe/providers.json
60+
61+
The updated pricing will be used instead of the embedded defaults.`,
62+
Run: runProvidersUpdate,
63+
}
64+
65+
func init() {
66+
rootCmd.AddCommand(providersCmd)
67+
providersCmd.AddCommand(providersListCmd)
68+
providersCmd.AddCommand(providersShowCmd)
69+
providersCmd.AddCommand(providersUpdateCmd)
70+
71+
providersListCmd.Flags().BoolVar(&providersJSONOutput, "json", false, "Output in JSON format")
72+
providersListCmd.Flags().BoolVarP(&providersVerbose, "verbose", "v", false, "Show detailed model information")
73+
74+
providersShowCmd.Flags().BoolVar(&providersJSONOutput, "json", false, "Output in JSON format")
75+
}
76+
77+
func runProvidersList(cmd *cobra.Command, args []string) {
78+
registry := providers.GetRegistry()
79+
allProviders := registry.ListProviders()
80+
81+
if len(allProviders) == 0 {
82+
fmt.Println("No providers found")
83+
return
84+
}
85+
86+
if providersJSONOutput {
87+
data, err := json.MarshalIndent(allProviders, "", " ")
88+
if err != nil {
89+
log.WithError(err).Error("failed to marshal providers to JSON")
90+
os.Exit(1)
91+
}
92+
fmt.Println(string(data))
93+
return
94+
}
95+
96+
// Human-readable table output
97+
config := registry.GetConfig()
98+
fmt.Printf("Provider Pricing Data (v%s)\n", config.Version)
99+
fmt.Printf("Updated: %s\n", config.UpdatedAt)
100+
fmt.Printf("Source: %s\n\n", config.Source)
101+
102+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
103+
fmt.Fprintln(w, "PROVIDER\tID\tMODELS\tDEFAULT LARGE\tDEFAULT SMALL")
104+
fmt.Fprintln(w, "--------\t--\t------\t-------------\t-------------")
105+
106+
for _, p := range allProviders {
107+
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\n",
108+
p.Name,
109+
p.ID,
110+
len(p.Models),
111+
truncate(p.DefaultLargeModelID, 30),
112+
truncate(p.DefaultSmallModelID, 30),
113+
)
114+
}
115+
w.Flush()
116+
117+
if providersVerbose {
118+
fmt.Println("\nModels by Provider:")
119+
for _, p := range allProviders {
120+
fmt.Printf("\n%s (%s):\n", p.Name, p.ID)
121+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
122+
fmt.Fprintln(w, " MODEL ID\tNAME\tINPUT $/1M\tOUTPUT $/1M\tCONTEXT")
123+
for _, m := range p.Models {
124+
fmt.Fprintf(w, " %s\t%s\t$%.2f\t$%.2f\t%d\n",
125+
truncate(m.ID, 40),
126+
truncate(m.Name, 30),
127+
m.CostPer1MIn,
128+
m.CostPer1MOut,
129+
m.ContextWindow,
130+
)
131+
}
132+
w.Flush()
133+
}
134+
} else {
135+
fmt.Println("\nUse --verbose to show detailed model information")
136+
}
137+
}
138+
139+
func runProvidersShow(cmd *cobra.Command, args []string) {
140+
providerID := args[0]
141+
registry := providers.GetRegistry()
142+
143+
provider, err := registry.GetProvider(providerID)
144+
if err != nil {
145+
log.WithError(err).Errorf("provider not found: %s", providerID)
146+
os.Exit(1)
147+
}
148+
149+
if providersJSONOutput {
150+
data, err := json.MarshalIndent(provider, "", " ")
151+
if err != nil {
152+
log.WithError(err).Error("failed to marshal provider to JSON")
153+
os.Exit(1)
154+
}
155+
fmt.Println(string(data))
156+
return
157+
}
158+
159+
// Human-readable output
160+
fmt.Printf("Provider: %s (%s)\n", provider.Name, provider.ID)
161+
fmt.Printf("Type: %s\n", provider.Type)
162+
if provider.APIEndpoint != "" {
163+
fmt.Printf("API Endpoint: %s\n", provider.APIEndpoint)
164+
}
165+
if provider.APIKey != "" {
166+
fmt.Printf("API Key: %s\n", provider.APIKey)
167+
}
168+
if provider.DefaultLargeModelID != "" {
169+
fmt.Printf("Default Large Model: %s\n", provider.DefaultLargeModelID)
170+
}
171+
if provider.DefaultSmallModelID != "" {
172+
fmt.Printf("Default Small Model: %s\n", provider.DefaultSmallModelID)
173+
}
174+
175+
fmt.Printf("\nModels (%d):\n", len(provider.Models))
176+
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
177+
fmt.Fprintln(w, "MODEL ID\tNAME\tINPUT $/1M\tOUTPUT $/1M\tCONTEXT\tREASON\tATTACH")
178+
179+
for _, m := range provider.Models {
180+
reason := "No"
181+
if m.CanReason {
182+
reason = "Yes"
183+
}
184+
attach := "No"
185+
if m.SupportsAttachments {
186+
attach = "Yes"
187+
}
188+
189+
fmt.Fprintf(w, "%s\t%s\t$%.2f\t$%.2f\t%d\t%s\t%s\n",
190+
m.ID,
191+
truncate(m.Name, 30),
192+
m.CostPer1MIn,
193+
m.CostPer1MOut,
194+
m.ContextWindow,
195+
reason,
196+
attach,
197+
)
198+
}
199+
w.Flush()
200+
}
201+
202+
func runProvidersUpdate(cmd *cobra.Command, args []string) {
203+
fmt.Println("Fetching latest provider configs from Catwalk...")
204+
205+
config, err := providers.FetchProvidersFromCatwalk()
206+
if err != nil {
207+
log.WithError(err).Error("failed to fetch provider configs")
208+
os.Exit(1)
209+
}
210+
211+
fmt.Printf("Successfully fetched %d providers\n", len(config.Providers))
212+
213+
// Save to ~/.agentpipe/providers.json
214+
homeDir, err := os.UserHomeDir()
215+
if err != nil {
216+
log.WithError(err).Error("failed to get home directory")
217+
os.Exit(1)
218+
}
219+
220+
agentpipeDir := filepath.Join(homeDir, ".agentpipe")
221+
if mkdirErr := os.MkdirAll(agentpipeDir, 0755); mkdirErr != nil {
222+
log.WithError(mkdirErr).Error("failed to create .agentpipe directory")
223+
os.Exit(1)
224+
}
225+
226+
outputPath := filepath.Join(agentpipeDir, "providers.json")
227+
data, marshalErr := json.MarshalIndent(config, "", " ")
228+
if marshalErr != nil {
229+
log.WithError(marshalErr).Error("failed to marshal config to JSON")
230+
os.Exit(1)
231+
}
232+
233+
if writeErr := os.WriteFile(outputPath, data, 0600); writeErr != nil {
234+
log.WithError(writeErr).Error("failed to write providers.json")
235+
os.Exit(1)
236+
}
237+
238+
fmt.Printf("Saved provider config to: %s\n", outputPath)
239+
fmt.Printf("Updated at: %s\n", config.UpdatedAt)
240+
fmt.Printf("Source: %s\n", config.Source)
241+
fmt.Println("\nProvider pricing data has been updated successfully!")
242+
243+
// Reload the registry to use the new config
244+
registry := providers.GetRegistry()
245+
if err := registry.Reload(); err != nil {
246+
log.WithError(err).Warn("failed to reload provider registry")
247+
} else {
248+
fmt.Println("Provider registry reloaded with new data")
249+
}
250+
}
251+
252+
func truncate(s string, maxLen int) string {
253+
if len(s) <= maxLen {
254+
return s
255+
}
256+
if maxLen <= 3 {
257+
return s[:maxLen]
258+
}
259+
return s[:maxLen-3] + "..."
260+
}
261+
262+
// Helper function to get provider summary

0 commit comments

Comments
 (0)