Skip to content

Commit fe9c430

Browse files
committed
docs: update claude.md with detailed vendoring guide
- Add step-by-step vendoring process - Document Portkey customizations needed after vendoring - Add wrapper update patterns for new params/methods - Add checklist for vendoring - Update version references to v2.16.0
1 parent d05765e commit fe9c430

1 file changed

Lines changed: 188 additions & 11 deletions

File tree

claude.md

Lines changed: 188 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ portkey-python-sdk/
1212
│ ├── __init__.py # Public API - exports all classes
1313
│ ├── version.py # SDK version
1414
│ ├── _vendor/ # Vendored dependencies
15-
│ │ └── openai/ # Vendored OpenAI SDK (e.g., v2.7.1)
15+
│ │ └── openai/ # Vendored OpenAI SDK (currently v2.16.0)
1616
│ ├── api_resources/ # Core SDK implementation
1717
│ │ ├── __init__.py # Internal exports
1818
│ │ ├── client.py # Portkey & AsyncPortkey clients
@@ -41,7 +41,7 @@ The SDK vendors the OpenAI Python SDK to avoid version conflicts:
4141
```toml
4242
# vendorize.toml
4343
target = "portkey_ai/_vendor"
44-
packages = ["openai==2.7.1"]
44+
packages = ["openai==2.16.0"] # Update this version as needed
4545
```
4646

4747
### Key Vendoring Quirks
@@ -407,19 +407,196 @@ portkey_ai/__init__.py # Final public export + add to __all__
407407

408408
## Updating Vendored OpenAI SDK
409409

410-
1. Update version in `vendorize.toml`:
411-
```toml
412-
packages = ["openai==X.Y.Z"]
410+
### Step-by-Step Process
411+
412+
#### Step 1: Compare SDK Versions
413+
Before vendoring, compare changes between versions:
414+
```
415+
https://github.com/openai/openai-python/compare/vOLD...vNEW
416+
```
417+
This helps identify:
418+
- New methods/resources added
419+
- Removed/deprecated methods
420+
- Signature changes in existing methods
421+
422+
#### Step 2: Clean and Re-vendor
423+
424+
```bash
425+
# Delete the old vendor folder contents
426+
rm -rf portkey_ai/_vendor/*
427+
428+
# Update version in vendorize.toml
429+
packages = ["openai==X.Y.Z"]
430+
431+
# Run vendorize (install with: pip install vendorize)
432+
python-vendorize
433+
```
434+
435+
The tool rewrites imports from `openai.*` to `portkey_ai._vendor.openai.*`
436+
437+
#### Step 3: Apply Portkey Customizations
438+
439+
**Two files MUST be modified after every vendoring:**
440+
441+
1. **`portkey_ai/_vendor/openai/_constants.py`**
442+
```python
443+
# Change DEFAULT_MAX_RETRIES from 2 to 1
444+
DEFAULT_MAX_RETRIES = 1
445+
```
446+
447+
2. **`portkey_ai/_vendor/openai/_base_client.py`**
448+
449+
Replace the `_should_retry` method with Portkey's custom logic:
450+
```python
451+
def _should_retry(self, response: httpx.Response) -> bool:
452+
# Custom Retry Conditions
453+
retry_status_code = response.status_code
454+
retry_trace_id = response.headers.get("x-portkey-trace-id")
455+
retry_request_id = response.headers.get("x-portkey-request-id")
456+
retry_gateway_exception = response.headers.get("x-portkey-gateway-exception")
457+
458+
if (
459+
retry_status_code < 500
460+
or retry_trace_id
461+
or retry_request_id
462+
or retry_gateway_exception
463+
):
464+
return False
465+
466+
return True
413467
```
414468

415-
2. Run vendorize tool (typically `vendorize` or `python -m vendorize`)
469+
#### Step 4: Run Lint to Find Issues
470+
471+
```bash
472+
make lint
473+
```
474+
475+
This will surface:
476+
- Type incompatibilities
477+
- Missing imports
478+
- Signature mismatches
479+
480+
#### Step 5: Update Portkey Wrappers
481+
482+
For each wrapper that needs updating:
483+
484+
**Adding new parameters:**
485+
```python
486+
# If wrapper explicitly lists parameters (not just **kwargs)
487+
# Add new params with Union[Type, Omit] = omit pattern
488+
def create(
489+
self,
490+
*,
491+
existing_param: Union[str, Omit] = omit,
492+
new_param: Union[str, Omit] = omit, # Add new param
493+
**kwargs,
494+
):
495+
response = self.openai_client.with_raw_response.resource.create(
496+
existing_param=existing_param,
497+
new_param=new_param, # Pass to underlying client
498+
...
499+
)
500+
```
501+
502+
**Adding new methods:**
503+
```python
504+
def new_method(
505+
self,
506+
*,
507+
param: Union[str, Omit] = omit,
508+
**kwargs,
509+
) -> NewResponseType:
510+
import json
511+
extra_headers = kwargs.pop("extra_headers", None)
512+
extra_query = kwargs.pop("extra_query", None)
513+
extra_body = kwargs.pop("extra_body", None)
514+
timeout = kwargs.pop("timeout", None)
515+
516+
response = self.openai_client.with_raw_response.resource.new_method(
517+
param=param,
518+
extra_headers=extra_headers,
519+
extra_query=extra_query,
520+
extra_body={**(extra_body or {}), **kwargs},
521+
timeout=timeout,
522+
)
523+
data = NewResponseType(**json.loads(response.text))
524+
data._headers = response.headers
525+
return data
526+
```
527+
528+
**Adding new response types:**
529+
```python
530+
# In api_resources/types/
531+
class NewResponseType(BaseModel, extra="allow"):
532+
id: str
533+
created_at: int
534+
# ... other fields from OpenAI type
535+
_headers: Optional[httpx.Headers] = PrivateAttr()
536+
537+
def get_headers(self) -> Optional[Dict[str, str]]:
538+
return parse_headers(self._headers)
539+
```
540+
541+
#### Step 6: Handle Type Ignore Comments
542+
543+
Sometimes Portkey uses more flexible types than OpenAI's strict literals:
544+
```python
545+
# Portkey allows any string, OpenAI expects specific literals
546+
response = self.openai_client.with_raw_response.resource.method(
547+
model=model, # type: ignore[arg-type]
548+
...
549+
)
550+
```
551+
552+
#### Step 7: Verify and Test
553+
554+
```bash
555+
# Format code
556+
make format
557+
558+
# Run all checks
559+
make lint
560+
561+
# Test imports
562+
python -c "from portkey_ai import Portkey; print('OK')"
416563

417-
3. The tool rewrites imports from `openai.*` to `portkey_ai._vendor.openai.*`
564+
# Run tests (requires valid virtual keys)
565+
pytest . -n 10
566+
```
567+
568+
### Wrapper Resilience Pattern
569+
570+
Most Portkey wrappers use `**kwargs` pattern which automatically forwards new parameters:
571+
572+
```python
573+
def create(self, *, name: str, **kwargs):
574+
extra_headers = kwargs.pop("extra_headers", None)
575+
# ... other pops
576+
response = self.openai_client.with_raw_response.resource.create(
577+
name=name,
578+
extra_headers=extra_headers,
579+
extra_body={**(extra_body or {}), **kwargs}, # Unknown params go here
580+
)
581+
```
418582

419-
4. After vendoring:
420-
- Check for new OpenAI resources that need Portkey wrappers
421-
- Update any Portkey types that mirror OpenAI types
422-
- Test thoroughly - OpenAI SDK changes can break wrappers
583+
This means many new OpenAI parameters work automatically without code changes. Only add explicit parameters when:
584+
1. The wrapper doesn't pass `**kwargs` to `extra_body`
585+
2. You want IDE autocomplete/type hints for important params
586+
3. The parameter needs special handling
587+
588+
### Checklist for Vendoring
589+
590+
- [ ] Delete `portkey_ai/_vendor/*`
591+
- [ ] Update `vendorize.toml` with new version
592+
- [ ] Run `python-vendorize`
593+
- [ ] Set `DEFAULT_MAX_RETRIES = 1` in `_constants.py`
594+
- [ ] Replace `_should_retry` in `_base_client.py`
595+
- [ ] Run `make lint` and fix errors
596+
- [ ] Add new methods/types if needed
597+
- [ ] Run `make format`
598+
- [ ] Test imports work
599+
- [ ] Commit vendored code first, then wrapper changes
423600

424601
## Common Gotchas
425602

0 commit comments

Comments
 (0)