cost-katana 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ """
2
+ Cost Katana - Unified AI Interface with Cost Optimization
3
+
4
+ Simple interface for AI models that routes through Cost Katana for:
5
+ - Cost optimization and tracking
6
+ - Automatic failover between providers
7
+ - Usage analytics and insights
8
+ - No API key management needed in your code
9
+
10
+ Example:
11
+ import cost_katana as ck
12
+
13
+ # Configure once
14
+ ck.configure(config_file='config.json')
15
+
16
+ # Use like any AI library
17
+ model = ck.GenerativeModel('gemini-2.0-flash')
18
+ chat = model.start_chat()
19
+ response = chat.send_message("Hello!")
20
+ print(response.text)
21
+ """
22
+
23
+ from .client import CostKatanaClient, get_global_client
24
+ from .models import GenerativeModel, ChatSession
25
+ from .exceptions import (
26
+ CostKatanaError,
27
+ AuthenticationError,
28
+ ModelNotAvailableError,
29
+ RateLimitError,
30
+ CostLimitExceededError
31
+ )
32
+ from .config import Config
33
+
34
+ __version__ = "1.0.0"
35
+ __all__ = [
36
+ "configure",
37
+ "GenerativeModel",
38
+ "ChatSession",
39
+ "CostKatanaClient",
40
+ "CostKatanaError",
41
+ "AuthenticationError",
42
+ "ModelNotAvailableError",
43
+ "RateLimitError",
44
+ "CostLimitExceededError",
45
+ "Config"
46
+ ]
47
+
48
+ # Import configure function from client
49
+ from .client import configure
50
+
51
+ def GenerativeModel(model_name: str, **kwargs):
52
+ """
53
+ Create a generative model instance.
54
+
55
+ Args:
56
+ model_name: Name of the model (e.g., 'gemini-2.0-flash', 'claude-3-sonnet', 'gpt-4')
57
+ **kwargs: Additional model configuration
58
+
59
+ Returns:
60
+ GenerativeModel instance
61
+
62
+ Example:
63
+ model = cost_katana.GenerativeModel('gemini-2.0-flash')
64
+ response = model.generate_content("Hello, world!")
65
+ """
66
+ client = get_global_client()
67
+
68
+ from .models import GenerativeModel as GM
69
+ return GM(client, model_name, **kwargs)
cost_katana/cli.py ADDED
@@ -0,0 +1,303 @@
1
+ """
2
+ Command-line interface for Cost Katana
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+ from rich.prompt import Prompt, Confirm
14
+ from rich.syntax import Syntax
15
+
16
+ try:
17
+ from . import configure, GenerativeModel, CostKatanaClient
18
+ from .config import Config
19
+ from .exceptions import CostKatanaError
20
+ except ImportError:
21
+ # Handle case when running as script
22
+ import cost_katana as ck
23
+ from cost_katana.config import Config
24
+ from cost_katana.exceptions import CostKatanaError
25
+
26
+ console = Console()
27
+
28
+ def create_sample_config():
29
+ """Create a sample configuration file"""
30
+ sample_config = {
31
+ "api_key": "dak_your_api_key_here",
32
+ "base_url": "https://api.costkatana.com",
33
+ "default_model": "gemini-2.0-flash",
34
+ "default_temperature": 0.7,
35
+ "default_max_tokens": 2000,
36
+ "cost_limit_per_day": 50.0,
37
+ "enable_analytics": True,
38
+ "enable_optimization": True,
39
+ "enable_failover": True,
40
+ "model_mappings": {
41
+ "gemini": "gemini-2.0-flash-exp",
42
+ "claude": "anthropic.claude-3-sonnet-20240229-v1:0",
43
+ "gpt4": "gpt-4-turbo-preview"
44
+ },
45
+ "providers": {
46
+ "google": {
47
+ "priority": 1,
48
+ "models": ["gemini-2.0-flash", "gemini-pro"]
49
+ },
50
+ "anthropic": {
51
+ "priority": 2,
52
+ "models": ["claude-3-sonnet", "claude-3-haiku"]
53
+ },
54
+ "openai": {
55
+ "priority": 3,
56
+ "models": ["gpt-4", "gpt-3.5-turbo"]
57
+ }
58
+ }
59
+ }
60
+ return sample_config
61
+
62
+ def init_config(args):
63
+ """Initialize configuration"""
64
+ config_path = Path(args.config or 'cost_katana_config.json')
65
+
66
+ if config_path.exists() and not args.force:
67
+ console.print(f"[yellow]Configuration file already exists: {config_path}[/yellow]")
68
+ if not Confirm.ask("Overwrite existing configuration?"):
69
+ return
70
+
71
+ console.print("[bold blue]Setting up Cost Katana configuration...[/bold blue]")
72
+
73
+ # Get API key
74
+ api_key = Prompt.ask(
75
+ "Enter your Cost Katana API key",
76
+ default=args.api_key if args.api_key else None
77
+ )
78
+
79
+ # Get base URL
80
+ base_url = Prompt.ask(
81
+ "Enter base URL",
82
+ default="https://api.costkatana.com"
83
+ )
84
+
85
+ # Get default model
86
+ default_model = Prompt.ask(
87
+ "Enter default model",
88
+ default="gemini-2.0-flash",
89
+ choices=["gemini-2.0-flash", "claude-3-sonnet", "gpt-4", "nova-pro"]
90
+ )
91
+
92
+ # Create configuration
93
+ config_data = create_sample_config()
94
+ config_data.update({
95
+ "api_key": api_key,
96
+ "base_url": base_url,
97
+ "default_model": default_model
98
+ })
99
+
100
+ # Save configuration
101
+ try:
102
+ with open(config_path, 'w') as f:
103
+ json.dump(config_data, f, indent=2)
104
+
105
+ console.print(f"[green]Configuration saved to: {config_path}[/green]")
106
+ console.print("\n[bold]Next steps:[/bold]")
107
+ console.print("1. Test the configuration: [cyan]cost-katana test[/cyan]")
108
+ console.print("2. Start a chat session: [cyan]cost-katana chat[/cyan]")
109
+ console.print("3. See available models: [cyan]cost-katana models[/cyan]")
110
+
111
+ except Exception as e:
112
+ console.print(f"[red]Failed to save configuration: {e}[/red]")
113
+ sys.exit(1)
114
+
115
+ def test_connection(args):
116
+ """Test connection to Cost Katana API"""
117
+ try:
118
+ config_path = args.config or 'cost_katana_config.json'
119
+
120
+ if Path(config_path).exists():
121
+ configure(config_file=config_path)
122
+ elif args.api_key:
123
+ configure(api_key=args.api_key)
124
+ else:
125
+ console.print("[red]No configuration found. Run 'cost-katana init' first.[/red]")
126
+ return
127
+
128
+ console.print("[bold blue]Testing Cost Katana connection...[/bold blue]")
129
+
130
+ # Test with a simple model
131
+ model = GenerativeModel('gemini-2.0-flash')
132
+ response = model.generate_content("Hello! Please respond with just 'OK' to test the connection.")
133
+
134
+ console.print(Panel(
135
+ f"[green]✓ Connection successful![/green]\n"
136
+ f"Model: {response.usage_metadata.model}\n"
137
+ f"Response: {response.text}\n"
138
+ f"Cost: ${response.usage_metadata.cost:.4f}\n"
139
+ f"Latency: {response.usage_metadata.latency:.2f}s",
140
+ title="Test Results"
141
+ ))
142
+
143
+ except Exception as e:
144
+ console.print(f"[red]✗ Connection failed: {e}[/red]")
145
+ sys.exit(1)
146
+
147
+ def list_models(args):
148
+ """List available models"""
149
+ try:
150
+ config_path = args.config or 'cost_katana_config.json'
151
+
152
+ if Path(config_path).exists():
153
+ configure(config_file=config_path)
154
+ elif args.api_key:
155
+ configure(api_key=args.api_key)
156
+ else:
157
+ console.print("[red]No configuration found. Run 'cost-katana init' first.[/red]")
158
+ return
159
+
160
+ client = CostKatanaClient(config_file=config_path if Path(config_path).exists() else None)
161
+ models = client.get_available_models()
162
+
163
+ table = Table(title="Available Models")
164
+ table.add_column("Model ID", style="cyan", no_wrap=True)
165
+ table.add_column("Display Name", style="magenta")
166
+ table.add_column("Provider", style="green")
167
+ table.add_column("Type", style="yellow")
168
+
169
+ for model in models:
170
+ model_id = model.get('id', model.get('modelId', 'Unknown'))
171
+ name = model.get('name', model.get('displayName', model_id))
172
+ provider = model.get('provider', 'Unknown')
173
+ model_type = model.get('type', 'Text')
174
+
175
+ table.add_row(model_id, name, provider, model_type)
176
+
177
+ console.print(table)
178
+
179
+ except Exception as e:
180
+ console.print(f"[red]Failed to fetch models: {e}[/red]")
181
+ sys.exit(1)
182
+
183
+ def start_chat(args):
184
+ """Start an interactive chat session"""
185
+ try:
186
+ config_path = args.config or 'cost_katana_config.json'
187
+
188
+ if Path(config_path).exists():
189
+ configure(config_file=config_path)
190
+ config = Config.from_file(config_path)
191
+ elif args.api_key:
192
+ configure(api_key=args.api_key)
193
+ config = Config(api_key=args.api_key)
194
+ else:
195
+ console.print("[red]No configuration found. Run 'cost-katana init' first.[/red]")
196
+ return
197
+
198
+ model_name = args.model or config.default_model
199
+
200
+ console.print(Panel(
201
+ f"[bold blue]Cost Katana Chat Session[/bold blue]\n"
202
+ f"Model: {model_name}\n"
203
+ f"Type 'quit' to exit, 'clear' to clear history",
204
+ title="Welcome"
205
+ ))
206
+
207
+ model = GenerativeModel(model_name)
208
+ chat = model.start_chat()
209
+
210
+ total_cost = 0.0
211
+
212
+ while True:
213
+ try:
214
+ message = Prompt.ask("[bold cyan]You[/bold cyan]")
215
+
216
+ if message.lower() in ['quit', 'exit', 'q']:
217
+ break
218
+ elif message.lower() == 'clear':
219
+ chat.clear_history()
220
+ console.print("[yellow]Chat history cleared.[/yellow]")
221
+ continue
222
+ elif message.lower() == 'cost':
223
+ console.print(f"[green]Total session cost: ${total_cost:.4f}[/green]")
224
+ continue
225
+
226
+ console.print("[bold green]Assistant[/bold green]: ", end="")
227
+
228
+ with console.status("Thinking..."):
229
+ response = chat.send_message(message)
230
+
231
+ console.print(response.text)
232
+
233
+ # Show cost info
234
+ total_cost += response.usage_metadata.cost
235
+ console.print(
236
+ f"[dim]Cost: ${response.usage_metadata.cost:.4f} | "
237
+ f"Total: ${total_cost:.4f} | "
238
+ f"Tokens: {response.usage_metadata.total_tokens}[/dim]\n"
239
+ )
240
+
241
+ except KeyboardInterrupt:
242
+ console.print("\n[yellow]Chat session interrupted.[/yellow]")
243
+ break
244
+ except Exception as e:
245
+ console.print(f"[red]Error: {e}[/red]")
246
+ continue
247
+
248
+ console.print(f"\n[bold]Session Summary:[/bold]")
249
+ console.print(f"Total Cost: ${total_cost:.4f}")
250
+ console.print("Thanks for using Cost Katana!")
251
+
252
+ except Exception as e:
253
+ console.print(f"[red]Failed to start chat: {e}[/red]")
254
+ sys.exit(1)
255
+
256
+ def main():
257
+ """Main CLI entry point"""
258
+ parser = argparse.ArgumentParser(
259
+ description="Cost Katana - Unified AI interface with cost optimization"
260
+ )
261
+ parser.add_argument(
262
+ '--config', '-c',
263
+ help='Configuration file path (default: cost_katana_config.json)'
264
+ )
265
+ parser.add_argument(
266
+ '--api-key', '-k',
267
+ help='Cost Katana API key'
268
+ )
269
+
270
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
271
+
272
+ # Init command
273
+ init_parser = subparsers.add_parser('init', help='Initialize configuration')
274
+ init_parser.add_argument('--force', action='store_true', help='Overwrite existing config')
275
+
276
+ # Test command
277
+ subparsers.add_parser('test', help='Test API connection')
278
+
279
+ # Models command
280
+ subparsers.add_parser('models', help='List available models')
281
+
282
+ # Chat command
283
+ chat_parser = subparsers.add_parser('chat', help='Start interactive chat')
284
+ chat_parser.add_argument('--model', '-m', help='Model to use for chat')
285
+
286
+ args = parser.parse_args()
287
+
288
+ if not args.command:
289
+ parser.print_help()
290
+ return
291
+
292
+ # Route to appropriate function
293
+ if args.command == 'init':
294
+ init_config(args)
295
+ elif args.command == 'test':
296
+ test_connection(args)
297
+ elif args.command == 'models':
298
+ list_models(args)
299
+ elif args.command == 'chat':
300
+ start_chat(args)
301
+
302
+ if __name__ == '__main__':
303
+ main()
cost_katana/client.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ Cost Katana HTTP Client
3
+ Handles communication with the Cost Katana backend API
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from typing import Dict, Any, Optional, List
9
+ import httpx
10
+ from .config import Config
11
+ from .exceptions import (
12
+ CostKatanaError,
13
+ AuthenticationError,
14
+ ModelNotAvailableError,
15
+ RateLimitError,
16
+ CostLimitExceededError
17
+ )
18
+
19
+ # Global client instance for the configure function
20
+ _global_client = None
21
+
22
+ def configure(
23
+ api_key: str = None,
24
+ base_url: str = None,
25
+ config_file: str = None,
26
+ **kwargs
27
+ ):
28
+ """
29
+ Configure Cost Katana client globally.
30
+
31
+ Args:
32
+ api_key: Your Cost Katana API key (starts with 'dak_')
33
+ base_url: Base URL for Cost Katana API (optional)
34
+ config_file: Path to JSON configuration file (optional)
35
+ **kwargs: Additional configuration options
36
+
37
+ Example:
38
+ # Using API key
39
+ cost_katana.configure(api_key='dak_your_key_here')
40
+
41
+ # Using config file
42
+ cost_katana.configure(config_file='config.json')
43
+ """
44
+ global _global_client
45
+ _global_client = CostKatanaClient(
46
+ api_key=api_key,
47
+ base_url=base_url,
48
+ config_file=config_file,
49
+ **kwargs
50
+ )
51
+ return _global_client
52
+
53
+ def get_global_client():
54
+ """Get the global client instance"""
55
+ if _global_client is None:
56
+ raise CostKatanaError("Cost Katana not configured. Call cost_katana.configure() first.")
57
+ return _global_client
58
+
59
+ class CostKatanaClient:
60
+ """HTTP client for Cost Katana API"""
61
+
62
+ def __init__(
63
+ self,
64
+ api_key: str = None,
65
+ base_url: str = None,
66
+ config_file: str = None,
67
+ timeout: int = 30,
68
+ **kwargs
69
+ ):
70
+ """
71
+ Initialize Cost Katana client.
72
+
73
+ Args:
74
+ api_key: Your Cost Katana API key
75
+ base_url: Base URL for the API
76
+ config_file: Path to JSON configuration file
77
+ timeout: Request timeout in seconds
78
+ """
79
+ self.config = Config.from_file(config_file) if config_file else Config()
80
+
81
+ # Override with provided parameters
82
+ if api_key:
83
+ self.config.api_key = api_key
84
+ if base_url:
85
+ self.config.base_url = base_url
86
+
87
+ # Apply additional config
88
+ for key, value in kwargs.items():
89
+ setattr(self.config, key, value)
90
+
91
+ # Validate configuration
92
+ if not self.config.api_key:
93
+ raise AuthenticationError(
94
+ "API key is required. Get one from https://costkatana.com/dashboard/api-keys"
95
+ )
96
+
97
+ # Initialize HTTP client
98
+ self.client = httpx.Client(
99
+ base_url=self.config.base_url,
100
+ timeout=timeout,
101
+ headers={
102
+ 'Authorization': f'Bearer {self.config.api_key}',
103
+ 'Content-Type': 'application/json',
104
+ 'User-Agent': f'cost-katana-python/1.0.0'
105
+ }
106
+ )
107
+
108
+ def __enter__(self):
109
+ return self
110
+
111
+ def __exit__(self, exc_type, exc_val, exc_tb):
112
+ self.close()
113
+
114
+ def close(self):
115
+ """Close the HTTP client"""
116
+ if hasattr(self, 'client'):
117
+ self.client.close()
118
+
119
+ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
120
+ """Handle HTTP response and raise appropriate exceptions"""
121
+ try:
122
+ data = response.json()
123
+ except json.JSONDecodeError:
124
+ raise CostKatanaError(f"Invalid JSON response: {response.text}")
125
+
126
+ if response.status_code == 401:
127
+ raise AuthenticationError(data.get('message', 'Authentication failed'))
128
+ elif response.status_code == 403:
129
+ raise AuthenticationError(data.get('message', 'Access forbidden'))
130
+ elif response.status_code == 404:
131
+ raise ModelNotAvailableError(data.get('message', 'Model not found'))
132
+ elif response.status_code == 429:
133
+ raise RateLimitError(data.get('message', 'Rate limit exceeded'))
134
+ elif response.status_code == 400 and 'cost' in data.get('message', '').lower():
135
+ raise CostLimitExceededError(data.get('message', 'Cost limit exceeded'))
136
+ elif not response.is_success:
137
+ raise CostKatanaError(
138
+ data.get('message', f'API error: {response.status_code}')
139
+ )
140
+
141
+ return data
142
+
143
+ def get_available_models(self) -> List[Dict[str, Any]]:
144
+ """Get list of available models"""
145
+ try:
146
+ response = self.client.get('/api/chat/models')
147
+ data = self._handle_response(response)
148
+ return data.get('data', [])
149
+ except Exception as e:
150
+ if isinstance(e, CostKatanaError):
151
+ raise
152
+ raise CostKatanaError(f"Failed to get models: {str(e)}")
153
+
154
+ def send_message(
155
+ self,
156
+ message: str,
157
+ model_id: str,
158
+ conversation_id: str = None,
159
+ temperature: float = 0.7,
160
+ max_tokens: int = 2000,
161
+ chat_mode: str = 'balanced',
162
+ use_multi_agent: bool = False,
163
+ **kwargs
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Send a message to the AI model via Cost Katana.
167
+
168
+ Args:
169
+ message: The message to send
170
+ model_id: ID of the model to use
171
+ conversation_id: Optional conversation ID
172
+ temperature: Sampling temperature (0.0 to 1.0)
173
+ max_tokens: Maximum tokens to generate
174
+ chat_mode: Chat optimization mode ('fastest', 'cheapest', 'balanced')
175
+ use_multi_agent: Whether to use multi-agent processing
176
+
177
+ Returns:
178
+ Response data from the API
179
+ """
180
+ payload = {
181
+ 'message': message,
182
+ 'modelId': model_id,
183
+ 'temperature': temperature,
184
+ 'maxTokens': max_tokens,
185
+ 'chatMode': chat_mode,
186
+ 'useMultiAgent': use_multi_agent,
187
+ **kwargs
188
+ }
189
+
190
+ if conversation_id:
191
+ payload['conversationId'] = conversation_id
192
+
193
+ try:
194
+ response = self.client.post('/api/chat/message', json=payload)
195
+ return self._handle_response(response)
196
+ except Exception as e:
197
+ if isinstance(e, CostKatanaError):
198
+ raise
199
+ raise CostKatanaError(f"Failed to send message: {str(e)}")
200
+
201
+ def create_conversation(self, title: str = None, model_id: str = None) -> Dict[str, Any]:
202
+ """Create a new conversation"""
203
+ payload = {}
204
+ if title:
205
+ payload['title'] = title
206
+ if model_id:
207
+ payload['modelId'] = model_id
208
+
209
+ try:
210
+ response = self.client.post('/api/chat/conversations', json=payload)
211
+ return self._handle_response(response)
212
+ except Exception as e:
213
+ if isinstance(e, CostKatanaError):
214
+ raise
215
+ raise CostKatanaError(f"Failed to create conversation: {str(e)}")
216
+
217
+ def get_conversation_history(self, conversation_id: str) -> Dict[str, Any]:
218
+ """Get conversation history"""
219
+ try:
220
+ response = self.client.get(f'/api/chat/conversations/{conversation_id}/history')
221
+ return self._handle_response(response)
222
+ except Exception as e:
223
+ if isinstance(e, CostKatanaError):
224
+ raise
225
+ raise CostKatanaError(f"Failed to get conversation history: {str(e)}")
226
+
227
+ def delete_conversation(self, conversation_id: str) -> Dict[str, Any]:
228
+ """Delete a conversation"""
229
+ try:
230
+ response = self.client.delete(f'/api/chat/conversations/{conversation_id}')
231
+ return self._handle_response(response)
232
+ except Exception as e:
233
+ if isinstance(e, CostKatanaError):
234
+ raise
235
+ raise CostKatanaError(f"Failed to delete conversation: {str(e)}")