codai-sdk 0.1.0__tar.gz
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.
- codai_sdk-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
- codai_sdk-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +17 -0
- codai_sdk-0.1.0/.github/dependabot.yml +11 -0
- codai_sdk-0.1.0/.github/pull_request_template.md +10 -0
- codai_sdk-0.1.0/.github/workflows/publish.yml +42 -0
- codai_sdk-0.1.0/.gitignore +8 -0
- codai_sdk-0.1.0/CHANGELOG.md +23 -0
- codai_sdk-0.1.0/CODE_OF_CONDUCT.md +31 -0
- codai_sdk-0.1.0/CONTRIBUTING.md +30 -0
- codai_sdk-0.1.0/LICENSE +21 -0
- codai_sdk-0.1.0/PKG-INFO +69 -0
- codai_sdk-0.1.0/README.md +48 -0
- codai_sdk-0.1.0/SECURITY.md +14 -0
- codai_sdk-0.1.0/examples/agent.py +16 -0
- codai_sdk-0.1.0/examples/chat.py +14 -0
- codai_sdk-0.1.0/examples/feedback.py +15 -0
- codai_sdk-0.1.0/examples/streaming.py +15 -0
- codai_sdk-0.1.0/pyproject.toml +31 -0
- codai_sdk-0.1.0/src/codai/__init__.py +338 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Report a problem with codai-sdk
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened?
|
|
9
|
+
description: A clear description of the bug and what you expected instead.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: repro
|
|
14
|
+
attributes:
|
|
15
|
+
label: Minimal reproduction
|
|
16
|
+
description: Smallest code snippet that reproduces the issue. Do NOT include your API key.
|
|
17
|
+
render: python
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: input
|
|
21
|
+
id: version
|
|
22
|
+
attributes:
|
|
23
|
+
label: SDK version
|
|
24
|
+
placeholder: "0.1.0"
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
27
|
+
- type: input
|
|
28
|
+
id: python
|
|
29
|
+
attributes:
|
|
30
|
+
label: Python version
|
|
31
|
+
placeholder: "3.12.2"
|
|
32
|
+
validations:
|
|
33
|
+
required: true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest an idea for codai-sdk
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: Problem
|
|
9
|
+
description: What are you trying to do that the SDK doesn't support today?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: Proposed solution
|
|
16
|
+
validations:
|
|
17
|
+
required: false
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: pypi
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Verify tag matches pyproject version
|
|
25
|
+
if: github.event_name == 'push'
|
|
26
|
+
run: |
|
|
27
|
+
PKG_VERSION="$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
|
|
28
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
29
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
30
|
+
echo "::error::tag $TAG_VERSION != pyproject $PKG_VERSION"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: |
|
|
36
|
+
python -m pip install --upgrade build
|
|
37
|
+
python -m build
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
41
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token needed once the
|
|
42
|
+
# trusted publisher is configured on pypi.org for this repo+workflow.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `codai-sdk` are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Initial public release.
|
|
14
|
+
- `Codai.chat()` — OpenAI-compatible completions with codai extensions
|
|
15
|
+
(`session_id`, `agent_mode`, `compact`, `best_of`).
|
|
16
|
+
- `Codai.chat_stream()` — streaming via generator.
|
|
17
|
+
- `Codai.agents_run()` — server-side agent loop.
|
|
18
|
+
- `Codai.feedback()` — thumbs rating on a completed request.
|
|
19
|
+
- `Codai.models()` — list models available to the key.
|
|
20
|
+
- Zero dependencies (stdlib only), Python 3.9+.
|
|
21
|
+
|
|
22
|
+
[Unreleased]: https://github.com/dragoscv/codai-sdk-python/compare/v0.1.0...HEAD
|
|
23
|
+
[0.1.0]: https://github.com/dragoscv/codai-sdk-python/releases/tag/v0.1.0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributor Code of Conduct
|
|
2
|
+
|
|
3
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
4
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
5
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
6
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
7
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
8
|
+
orientation.
|
|
9
|
+
|
|
10
|
+
## Our Standards
|
|
11
|
+
|
|
12
|
+
Examples of behavior that contributes to a positive environment:
|
|
13
|
+
|
|
14
|
+
- Demonstrating empathy and kindness toward other people
|
|
15
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
16
|
+
- Giving and gracefully accepting constructive feedback
|
|
17
|
+
|
|
18
|
+
Examples of unacceptable behavior:
|
|
19
|
+
|
|
20
|
+
- Trolling, insulting or derogatory comments, and personal attacks
|
|
21
|
+
- Public or private harassment
|
|
22
|
+
- Publishing others' private information without explicit permission
|
|
23
|
+
|
|
24
|
+
## Enforcement
|
|
25
|
+
|
|
26
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
27
|
+
reported to the maintainers at **conduct@codai.ro**. All complaints will be
|
|
28
|
+
reviewed and investigated promptly and fairly.
|
|
29
|
+
|
|
30
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
|
31
|
+
version 2.1.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in `codai-sdk`!
|
|
4
|
+
|
|
5
|
+
> **Note:** This repository is a published mirror of the SDK developed inside
|
|
6
|
+
> the codai monorepo. Source changes are made upstream and synced here on
|
|
7
|
+
> release. Issues and PRs are welcome and will be triaged upstream.
|
|
8
|
+
|
|
9
|
+
## Reporting issues
|
|
10
|
+
|
|
11
|
+
Open an issue with a minimal reproduction, the SDK version, and your Python
|
|
12
|
+
version.
|
|
13
|
+
|
|
14
|
+
## Pull requests
|
|
15
|
+
|
|
16
|
+
- Keep changes focused and small.
|
|
17
|
+
- Keep the SDK **dependency-free** (stdlib only).
|
|
18
|
+
- Add or update tests where it makes sense.
|
|
19
|
+
- Do not include any credentials in code, tests, or examples.
|
|
20
|
+
|
|
21
|
+
## Local development
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install --upgrade build
|
|
25
|
+
python -m build # produces wheel + sdist in dist/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Code of Conduct
|
|
29
|
+
|
|
30
|
+
By participating you agree to abide by the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
codai_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dragos Catalin Vladulescu (codai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
codai_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codai-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the codai AI gateway — chat, streaming, agents, feedback, models.
|
|
5
|
+
Project-URL: Homepage, https://codai.ro
|
|
6
|
+
Project-URL: Repository, https://github.com/dragoscv/codai-sdk-python
|
|
7
|
+
Project-URL: Issues, https://github.com/dragoscv/codai-sdk-python/issues
|
|
8
|
+
Author-email: codai <support@codai.ro>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,codai,gateway,llm,openai-compatible
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# codai-sdk — Python SDK
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
25
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
26
|
+
[](./LICENSE)
|
|
27
|
+
|
|
28
|
+
Official Python client for the [codai](https://codai.ro) AI gateway.
|
|
29
|
+
Zero dependencies (stdlib only), Python 3.9+.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install codai-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> You need a codai API key. Get one at **[codai.ro](https://codai.ro)**.
|
|
36
|
+
> The import name stays `codai`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from codai import Codai
|
|
40
|
+
|
|
41
|
+
client = Codai(api_key="ck-...", session_id="my-project")
|
|
42
|
+
|
|
43
|
+
# Chat (OpenAI-compatible, smart-routed)
|
|
44
|
+
result = client.chat([{"role": "user", "content": "Explain asyncio.gather"}])
|
|
45
|
+
print(result.content)
|
|
46
|
+
print(result.routed_to) # which model actually served
|
|
47
|
+
|
|
48
|
+
# Streaming
|
|
49
|
+
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
|
|
50
|
+
print(delta, end="", flush=True)
|
|
51
|
+
|
|
52
|
+
# Server-side agent loop
|
|
53
|
+
run = client.agents_run("Find and summarize the TODOs in this codebase")
|
|
54
|
+
print(run.result)
|
|
55
|
+
|
|
56
|
+
# Feedback (improves routing for everyone)
|
|
57
|
+
client.feedback(result.request_id, 1)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## codai extensions
|
|
61
|
+
|
|
62
|
+
| Option | Effect |
|
|
63
|
+
| ----------------- | ------------------------------------ |
|
|
64
|
+
| `session_id` | Session memory + routing stickiness |
|
|
65
|
+
| `agent_mode=True` | Plan-and-execute loop (Pro+) |
|
|
66
|
+
| `compact="auto"` | Server-side context compaction |
|
|
67
|
+
| `best_of=0/3` | Disable / force best-of-N ensembling |
|
|
68
|
+
|
|
69
|
+
MIT © codai
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# codai-sdk — Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
4
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Official Python client for the [codai](https://codai.ro) AI gateway.
|
|
8
|
+
Zero dependencies (stdlib only), Python 3.9+.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install codai-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> You need a codai API key. Get one at **[codai.ro](https://codai.ro)**.
|
|
15
|
+
> The import name stays `codai`:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from codai import Codai
|
|
19
|
+
|
|
20
|
+
client = Codai(api_key="ck-...", session_id="my-project")
|
|
21
|
+
|
|
22
|
+
# Chat (OpenAI-compatible, smart-routed)
|
|
23
|
+
result = client.chat([{"role": "user", "content": "Explain asyncio.gather"}])
|
|
24
|
+
print(result.content)
|
|
25
|
+
print(result.routed_to) # which model actually served
|
|
26
|
+
|
|
27
|
+
# Streaming
|
|
28
|
+
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
|
|
29
|
+
print(delta, end="", flush=True)
|
|
30
|
+
|
|
31
|
+
# Server-side agent loop
|
|
32
|
+
run = client.agents_run("Find and summarize the TODOs in this codebase")
|
|
33
|
+
print(run.result)
|
|
34
|
+
|
|
35
|
+
# Feedback (improves routing for everyone)
|
|
36
|
+
client.feedback(result.request_id, 1)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## codai extensions
|
|
40
|
+
|
|
41
|
+
| Option | Effect |
|
|
42
|
+
| ----------------- | ------------------------------------ |
|
|
43
|
+
| `session_id` | Session memory + routing stickiness |
|
|
44
|
+
| `agent_mode=True` | Plan-and-execute loop (Pro+) |
|
|
45
|
+
| `compact="auto"` | Server-side context compaction |
|
|
46
|
+
| `best_of=0/3` | Disable / force best-of-N ensembling |
|
|
47
|
+
|
|
48
|
+
MIT © codai
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please **do not** open public issues for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Email **security@codai.ro** with details and reproduction steps. We aim to
|
|
8
|
+
acknowledge reports within 72 hours.
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
This repository contains the public client SDK only. It holds no credentials
|
|
13
|
+
and no server-side logic. Never commit your codai API key — load it from an
|
|
14
|
+
environment variable (e.g. `CODAI_API_KEY`).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Server-side agent run.
|
|
2
|
+
|
|
3
|
+
CODAI_API_KEY=sk_... python examples/agent.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from codai import Codai
|
|
8
|
+
|
|
9
|
+
client = Codai(api_key=os.environ["CODAI_API_KEY"])
|
|
10
|
+
|
|
11
|
+
run = client.agents_run(
|
|
12
|
+
"Summarize the following text in 3 bullet points.",
|
|
13
|
+
context="codai is an OpenAI-compatible AI gateway with smart routing.",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
print(run.result)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Basic chat completion.
|
|
2
|
+
|
|
3
|
+
CODAI_API_KEY=sk_... python examples/chat.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from codai import Codai
|
|
8
|
+
|
|
9
|
+
client = Codai(api_key=os.environ["CODAI_API_KEY"])
|
|
10
|
+
|
|
11
|
+
result = client.chat([{"role": "user", "content": "Give me one tip for clean Python."}])
|
|
12
|
+
|
|
13
|
+
print(result.content)
|
|
14
|
+
print("routed to:", result.routed_to)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Submit feedback on a completion.
|
|
2
|
+
|
|
3
|
+
CODAI_API_KEY=sk_... python examples/feedback.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from codai import Codai
|
|
8
|
+
|
|
9
|
+
client = Codai(api_key=os.environ["CODAI_API_KEY"])
|
|
10
|
+
|
|
11
|
+
result = client.chat([{"role": "user", "content": "Hello!"}])
|
|
12
|
+
|
|
13
|
+
if result.request_id:
|
|
14
|
+
client.feedback(result.request_id, 1) # 1 = up, -1 = down
|
|
15
|
+
print("feedback submitted for", result.request_id)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Streaming chat completion.
|
|
2
|
+
|
|
3
|
+
CODAI_API_KEY=sk_... python examples/streaming.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from codai import Codai
|
|
9
|
+
|
|
10
|
+
client = Codai(api_key=os.environ["CODAI_API_KEY"])
|
|
11
|
+
|
|
12
|
+
for delta in client.chat_stream([{"role": "user", "content": "Write a haiku about Python."}]):
|
|
13
|
+
sys.stdout.write(delta)
|
|
14
|
+
sys.stdout.flush()
|
|
15
|
+
print()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codai-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the codai AI gateway — chat, streaming, agents, feedback, models."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "codai", email = "support@codai.ro" }]
|
|
13
|
+
keywords = ["codai", "ai", "llm", "openai-compatible", "gateway", "agents"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://codai.ro"
|
|
27
|
+
Repository = "https://github.com/dragoscv/codai-sdk-python"
|
|
28
|
+
Issues = "https://github.com/dragoscv/codai-sdk-python/issues"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/codai"]
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""codai — official Python SDK for the codai AI gateway.
|
|
2
|
+
|
|
3
|
+
Zero-dependency (stdlib urllib) client mirroring @codai/sdk:
|
|
4
|
+
|
|
5
|
+
from codai import Codai
|
|
6
|
+
|
|
7
|
+
client = Codai(api_key="ck-...", session_id="my-session")
|
|
8
|
+
result = client.chat([{"role": "user", "content": "hello"}])
|
|
9
|
+
print(result.content, result.routed_to)
|
|
10
|
+
|
|
11
|
+
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
|
|
12
|
+
print(delta, end="")
|
|
13
|
+
|
|
14
|
+
run = client.agents_run("summarize the repo structure")
|
|
15
|
+
client.feedback(result.request_id, 1)
|
|
16
|
+
|
|
17
|
+
Memory tools (memory_recall/memory_remember) and skills are GATEWAY-NATIVE:
|
|
18
|
+
pass a stable session_id and the model uses them server-side.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import time
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any, Dict, Generator, List, Optional
|
|
29
|
+
|
|
30
|
+
__all__ = ["Codai", "CodaiError", "ChatResult", "AgentRunResult"]
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
_RETRYABLE = {429, 500, 502, 503, 504}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodaiError(Exception):
|
|
37
|
+
def __init__(self, message: str, status: int = 0, body: Any = None) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
self.body = body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ChatResult:
|
|
45
|
+
content: str
|
|
46
|
+
raw: Dict[str, Any]
|
|
47
|
+
request_id: Optional[str]
|
|
48
|
+
routed_to: Optional[str]
|
|
49
|
+
exec_verify: Optional[str]
|
|
50
|
+
usage: Optional[Dict[str, int]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AgentRunResult:
|
|
55
|
+
result: str
|
|
56
|
+
model: str
|
|
57
|
+
event_id: Optional[str]
|
|
58
|
+
usage: Optional[Dict[str, Any]] = field(default=None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Codai:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: str,
|
|
65
|
+
base_url: str = "https://ai.codai.ro",
|
|
66
|
+
session_id: Optional[str] = None,
|
|
67
|
+
timeout: float = 120.0,
|
|
68
|
+
max_retries: int = 2,
|
|
69
|
+
) -> None:
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise ValueError("Codai: api_key is required")
|
|
72
|
+
self.api_key = api_key
|
|
73
|
+
self.base_url = base_url.rstrip("/")
|
|
74
|
+
self.session_id = session_id
|
|
75
|
+
self.timeout = timeout
|
|
76
|
+
self.max_retries = max_retries
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------- chat
|
|
79
|
+
def chat(
|
|
80
|
+
self,
|
|
81
|
+
messages: List[Dict[str, Any]],
|
|
82
|
+
model: str = "codai",
|
|
83
|
+
temperature: Optional[float] = None,
|
|
84
|
+
max_tokens: Optional[int] = None,
|
|
85
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
86
|
+
agent_mode: bool = False,
|
|
87
|
+
compact: Optional[str] = None,
|
|
88
|
+
best_of: Optional[int] = None,
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
) -> ChatResult:
|
|
91
|
+
body: Dict[str, Any] = {"model": model, "messages": messages, "stream": False}
|
|
92
|
+
if temperature is not None:
|
|
93
|
+
body["temperature"] = temperature
|
|
94
|
+
if max_tokens is not None:
|
|
95
|
+
body["max_tokens"] = max_tokens
|
|
96
|
+
if tools:
|
|
97
|
+
body["tools"] = tools
|
|
98
|
+
headers = self._chat_headers(agent_mode, compact, best_of, session_id)
|
|
99
|
+
status, resp_headers, data = self._request(
|
|
100
|
+
"/v1/chat/completions", body, extra_headers=headers
|
|
101
|
+
)
|
|
102
|
+
raw = json.loads(data)
|
|
103
|
+
choices = raw.get("choices") or []
|
|
104
|
+
usage_raw = raw.get("usage") or {}
|
|
105
|
+
return ChatResult(
|
|
106
|
+
content=str((choices[0].get("message") or {}).get("content") or "")
|
|
107
|
+
if choices
|
|
108
|
+
else "",
|
|
109
|
+
raw=raw,
|
|
110
|
+
request_id=resp_headers.get("x-request-id"),
|
|
111
|
+
routed_to=resp_headers.get("x-codai-routed-to"),
|
|
112
|
+
exec_verify=resp_headers.get("x-codai-exec-verify"),
|
|
113
|
+
usage=(
|
|
114
|
+
{
|
|
115
|
+
"prompt_tokens": int(usage_raw.get("prompt_tokens") or 0),
|
|
116
|
+
"completion_tokens": int(usage_raw.get("completion_tokens") or 0),
|
|
117
|
+
}
|
|
118
|
+
if usage_raw
|
|
119
|
+
else None
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def chat_stream(
|
|
124
|
+
self,
|
|
125
|
+
messages: List[Dict[str, Any]],
|
|
126
|
+
model: str = "codai",
|
|
127
|
+
temperature: Optional[float] = None,
|
|
128
|
+
max_tokens: Optional[int] = None,
|
|
129
|
+
agent_mode: bool = False,
|
|
130
|
+
session_id: Optional[str] = None,
|
|
131
|
+
) -> Generator[str, None, None]:
|
|
132
|
+
body: Dict[str, Any] = {"model": model, "messages": messages, "stream": True}
|
|
133
|
+
if temperature is not None:
|
|
134
|
+
body["temperature"] = temperature
|
|
135
|
+
if max_tokens is not None:
|
|
136
|
+
body["max_tokens"] = max_tokens
|
|
137
|
+
headers = self._chat_headers(agent_mode, None, None, session_id)
|
|
138
|
+
req = self._build_request("/v1/chat/completions", body, headers)
|
|
139
|
+
try:
|
|
140
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
141
|
+
buffer = b""
|
|
142
|
+
for chunk in iter(lambda: resp.read(1024), b""):
|
|
143
|
+
buffer += chunk
|
|
144
|
+
while b"\n\n" in buffer:
|
|
145
|
+
frame, buffer = buffer.split(b"\n\n", 1)
|
|
146
|
+
for line in frame.split(b"\n"):
|
|
147
|
+
if not line.startswith(b"data:"):
|
|
148
|
+
continue
|
|
149
|
+
payload = line[5:].strip()
|
|
150
|
+
if payload == b"[DONE]":
|
|
151
|
+
return
|
|
152
|
+
try:
|
|
153
|
+
parsed = json.loads(payload)
|
|
154
|
+
delta = (
|
|
155
|
+
(parsed.get("choices") or [{}])[0]
|
|
156
|
+
.get("delta", {})
|
|
157
|
+
.get("content")
|
|
158
|
+
)
|
|
159
|
+
if delta:
|
|
160
|
+
yield delta
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
continue
|
|
163
|
+
except urllib.error.HTTPError as e: # pragma: no cover
|
|
164
|
+
raise CodaiError(f"codai stream → {e.code}", e.code, e.read()) from e
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------ agents
|
|
167
|
+
def agents_run(
|
|
168
|
+
self,
|
|
169
|
+
task: str,
|
|
170
|
+
context: Optional[str] = None,
|
|
171
|
+
system: Optional[str] = None,
|
|
172
|
+
model: Optional[str] = None,
|
|
173
|
+
) -> AgentRunResult:
|
|
174
|
+
body: Dict[str, Any] = {"task": task}
|
|
175
|
+
if context:
|
|
176
|
+
body["context"] = context
|
|
177
|
+
if system:
|
|
178
|
+
body["system"] = system
|
|
179
|
+
if model:
|
|
180
|
+
body["model"] = model
|
|
181
|
+
_, _, data = self._request("/v1/agents/run", body)
|
|
182
|
+
raw = json.loads(data)
|
|
183
|
+
return AgentRunResult(
|
|
184
|
+
result=str(raw.get("result") or ""),
|
|
185
|
+
model=str(raw.get("model") or ""),
|
|
186
|
+
event_id=raw.get("event_id"),
|
|
187
|
+
usage=raw.get("usage"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------- feedback
|
|
191
|
+
def feedback(self, request_id: str, rating: int, comment: Optional[str] = None) -> None:
|
|
192
|
+
body: Dict[str, Any] = {"event_id": request_id, "rating": rating}
|
|
193
|
+
if comment:
|
|
194
|
+
body["comment"] = comment
|
|
195
|
+
self._request("/v1/feedback", body)
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------ models
|
|
198
|
+
def models(self) -> List[Dict[str, Any]]:
|
|
199
|
+
_, _, data = self._request("/v1/models", None, method="GET")
|
|
200
|
+
return json.loads(data).get("data") or []
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------ tokens
|
|
203
|
+
def mint_token(self, scope: str, ttl_seconds: Optional[int] = None) -> Dict[str, Any]:
|
|
204
|
+
"""Mint an ephemeral scoped token ('realtime'|'audio'|'embeddings').
|
|
205
|
+
|
|
206
|
+
Returns {"token", "expires_at", "scope"}. Hand the token to a
|
|
207
|
+
browser/webview client; it expires in 60-3600s and only works on
|
|
208
|
+
its scope's surface.
|
|
209
|
+
"""
|
|
210
|
+
body: Dict[str, Any] = {"scope": scope}
|
|
211
|
+
if ttl_seconds is not None:
|
|
212
|
+
body["ttl_seconds"] = ttl_seconds
|
|
213
|
+
_, _, data = self._request("/v1/tokens", body)
|
|
214
|
+
return json.loads(data)
|
|
215
|
+
|
|
216
|
+
# -------------------------------------------------------- embeddings
|
|
217
|
+
def embeddings(
|
|
218
|
+
self,
|
|
219
|
+
input: Any,
|
|
220
|
+
model: str = "codai-embed",
|
|
221
|
+
dimensions: Optional[int] = None,
|
|
222
|
+
) -> List[List[float]]:
|
|
223
|
+
body: Dict[str, Any] = {"model": model, "input": input}
|
|
224
|
+
if dimensions is not None:
|
|
225
|
+
body["dimensions"] = dimensions
|
|
226
|
+
_, _, data = self._request("/v1/embeddings", body)
|
|
227
|
+
return [d["embedding"] for d in json.loads(data).get("data") or []]
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------- audio
|
|
230
|
+
def transcribe(
|
|
231
|
+
self,
|
|
232
|
+
file_bytes: bytes,
|
|
233
|
+
filename: str = "audio.webm",
|
|
234
|
+
model: str = "codai-transcribe",
|
|
235
|
+
) -> str:
|
|
236
|
+
"""Speech-to-text via multipart upload (max 25 MB). Returns transcript."""
|
|
237
|
+
boundary = f"----codai{int(time.time() * 1000)}"
|
|
238
|
+
parts = []
|
|
239
|
+
parts.append(
|
|
240
|
+
f"--{boundary}\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\n{model}\r\n".encode()
|
|
241
|
+
)
|
|
242
|
+
parts.append(
|
|
243
|
+
(
|
|
244
|
+
f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; "
|
|
245
|
+
f"filename=\"{filename}\"\r\nContent-Type: application/octet-stream\r\n\r\n"
|
|
246
|
+
).encode()
|
|
247
|
+
)
|
|
248
|
+
parts.append(file_bytes)
|
|
249
|
+
parts.append(f"\r\n--{boundary}--\r\n".encode())
|
|
250
|
+
payload = b"".join(parts)
|
|
251
|
+
req = urllib.request.Request(
|
|
252
|
+
f"{self.base_url}/v1/audio/transcriptions",
|
|
253
|
+
data=payload,
|
|
254
|
+
headers={
|
|
255
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
256
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
257
|
+
},
|
|
258
|
+
method="POST",
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
262
|
+
return str(json.loads(resp.read()).get("text") or "")
|
|
263
|
+
except urllib.error.HTTPError as e:
|
|
264
|
+
try:
|
|
265
|
+
err_body = json.loads(e.read())
|
|
266
|
+
except Exception: # noqa: BLE001
|
|
267
|
+
err_body = None
|
|
268
|
+
raise CodaiError(f"codai /v1/audio/transcriptions → {e.code}", e.code, err_body) from e
|
|
269
|
+
|
|
270
|
+
def speech(
|
|
271
|
+
self,
|
|
272
|
+
input: str,
|
|
273
|
+
model: str = "codai-tts",
|
|
274
|
+
voice: str = "alloy",
|
|
275
|
+
) -> bytes:
|
|
276
|
+
"""Text-to-speech. Returns audio bytes (default mp3)."""
|
|
277
|
+
_, _, data = self._request(
|
|
278
|
+
"/v1/audio/speech", {"model": model, "input": input, "voice": voice}
|
|
279
|
+
)
|
|
280
|
+
return data
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------- internal
|
|
283
|
+
def _chat_headers(
|
|
284
|
+
self,
|
|
285
|
+
agent_mode: bool,
|
|
286
|
+
compact: Optional[str],
|
|
287
|
+
best_of: Optional[int],
|
|
288
|
+
session_id: Optional[str],
|
|
289
|
+
) -> Dict[str, str]:
|
|
290
|
+
h: Dict[str, str] = {}
|
|
291
|
+
session = session_id or self.session_id
|
|
292
|
+
if session:
|
|
293
|
+
h["x-codai-session-id"] = session
|
|
294
|
+
if agent_mode:
|
|
295
|
+
h["x-codai-mode"] = "agent"
|
|
296
|
+
if compact:
|
|
297
|
+
h["x-codai-compact"] = compact
|
|
298
|
+
if best_of is not None:
|
|
299
|
+
h["x-codai-best-of"] = str(best_of)
|
|
300
|
+
return h
|
|
301
|
+
|
|
302
|
+
def _build_request(
|
|
303
|
+
self, path: str, body: Any, extra_headers: Optional[Dict[str, str]] = None, method: str = "POST"
|
|
304
|
+
) -> urllib.request.Request:
|
|
305
|
+
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
306
|
+
data = None
|
|
307
|
+
if body is not None:
|
|
308
|
+
headers["Content-Type"] = "application/json"
|
|
309
|
+
data = json.dumps(body).encode()
|
|
310
|
+
headers.update(extra_headers or {})
|
|
311
|
+
return urllib.request.Request(
|
|
312
|
+
f"{self.base_url}{path}", data=data, headers=headers, method=method
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _request(
|
|
316
|
+
self, path: str, body: Any, extra_headers: Optional[Dict[str, str]] = None, method: str = "POST"
|
|
317
|
+
) -> tuple:
|
|
318
|
+
last_err: Optional[Exception] = None
|
|
319
|
+
for attempt in range(self.max_retries + 1):
|
|
320
|
+
req = self._build_request(path, body, extra_headers, method)
|
|
321
|
+
try:
|
|
322
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
323
|
+
return resp.status, dict(resp.headers), resp.read()
|
|
324
|
+
except urllib.error.HTTPError as e:
|
|
325
|
+
if e.code in _RETRYABLE and attempt < self.max_retries:
|
|
326
|
+
time.sleep(0.5 * 2**attempt)
|
|
327
|
+
continue
|
|
328
|
+
try:
|
|
329
|
+
err_body = json.loads(e.read())
|
|
330
|
+
except Exception: # noqa: BLE001
|
|
331
|
+
err_body = None
|
|
332
|
+
raise CodaiError(f"codai {path} → {e.code}", e.code, err_body) from e
|
|
333
|
+
except Exception as e: # noqa: BLE001 — network-level
|
|
334
|
+
last_err = e
|
|
335
|
+
if attempt < self.max_retries:
|
|
336
|
+
time.sleep(0.5 * 2**attempt)
|
|
337
|
+
continue
|
|
338
|
+
raise CodaiError(f"codai {path} failed after retries: {last_err}")
|