ocai-openai 0.1.1__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.
- ocai_openai-0.1.1/.gitignore +60 -0
- ocai_openai-0.1.1/CHANGELOG.md +28 -0
- ocai_openai-0.1.1/PKG-INFO +71 -0
- ocai_openai-0.1.1/README.md +56 -0
- ocai_openai-0.1.1/pyproject.toml +39 -0
- ocai_openai-0.1.1/src/ocai/openai/__init__.py +53 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/__init__.py +110 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/client.py +210 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/client_stream.py +251 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/codec.py +1989 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/edges.py +444 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/variant.py +41 -0
- ocai_openai-0.1.1/src/ocai/openai/chat_completions/wire.py +945 -0
- ocai_openai-0.1.1/src/ocai/openai/py.typed +0 -0
- ocai_openai-0.1.1/src/ocai/openai/responses/__init__.py +41 -0
- ocai_openai-0.1.1/src/ocai/openai/responses/codec.py +158 -0
- ocai_openai-0.1.1/src/ocai/openai/responses/edges.py +60 -0
- ocai_openai-0.1.1/src/ocai/openai/responses/wire.py +49 -0
- ocai_openai-0.1.1/tests/test_conformance.py +554 -0
- ocai_openai-0.1.1/tests/test_gateway_requests.py +77 -0
- ocai_openai-0.1.1/tests/test_gateway_responses.py +96 -0
- ocai_openai-0.1.1/tests/test_gateway_streams.py +126 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# uv / venv
|
|
11
|
+
.venv/
|
|
12
|
+
uv.lock.bak
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.import_linter_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# Docs site build output and page cache (zensical build)
|
|
23
|
+
/site/
|
|
24
|
+
/.cache/
|
|
25
|
+
|
|
26
|
+
# Node (future typescript/)
|
|
27
|
+
node_modules/
|
|
28
|
+
*.tsbuildinfo
|
|
29
|
+
|
|
30
|
+
# Go (future go/)
|
|
31
|
+
/go/bin/
|
|
32
|
+
|
|
33
|
+
# Editors / OS. Shared editor settings ARE committed: the multi-root workspace
|
|
34
|
+
# (ocai.code-workspace) and each folder's .vscode/settings.json + extensions.json
|
|
35
|
+
# give Pylance/Ruff the right interpreter per project. Personal files stay local.
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/*
|
|
38
|
+
!.vscode/settings.json
|
|
39
|
+
!.vscode/extensions.json
|
|
40
|
+
.DS_Store
|
|
41
|
+
|
|
42
|
+
# Per-user agent settings (never commit personal permissions)
|
|
43
|
+
.claude/settings.local.json
|
|
44
|
+
.codex/config.toml
|
|
45
|
+
|
|
46
|
+
# Platform build and browser-test artifacts
|
|
47
|
+
.nuxt/
|
|
48
|
+
.output/
|
|
49
|
+
platform/test-results/
|
|
50
|
+
platform/playwright-report/
|
|
51
|
+
|
|
52
|
+
# Local environment files: the root .env is the active one, .envs/ holds the
|
|
53
|
+
# per-environment variants (`task env -- <ENV>` copies .envs/.env.<ENV> to .env).
|
|
54
|
+
# Never committed: they hold credentials.
|
|
55
|
+
/.env
|
|
56
|
+
/.envs/
|
|
57
|
+
.local/
|
|
58
|
+
|
|
59
|
+
# Task execution fingerprints
|
|
60
|
+
.task/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog — ocai-openai
|
|
2
|
+
|
|
3
|
+
All notable changes to the `ocai-openai` distribution. Generated from the change files in
|
|
4
|
+
`.changeset/` by the release workflow (ADR 0010); do not edit released sections by hand.
|
|
5
|
+
## 0.1.1 (2026-11-11)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
#### Admit provider-executed server-tool definitions with honest support reports
|
|
10
|
+
|
|
11
|
+
Admit provider-executed server-tool definitions with honest support reports. (WU-41, #80)
|
|
12
|
+
|
|
13
|
+
#### Preserve scoped request and replay anchors
|
|
14
|
+
|
|
15
|
+
Preserve scoped request and replay anchors. (WU-42, #83)
|
|
16
|
+
|
|
17
|
+
#### Qualify independent DeepSeek unary conversion
|
|
18
|
+
|
|
19
|
+
Share compatible mapping through explicit variant configuration while preserving
|
|
20
|
+
DeepSeek identity, mandatory reasoning replay, honest parameter effects and
|
|
21
|
+
measured usage. Add independently censused request encode/audit and response
|
|
22
|
+
decode edges with recoverable unknown-field evidence. (WU-44b, #91)
|
|
23
|
+
|
|
24
|
+
### Fixes
|
|
25
|
+
|
|
26
|
+
#### Package metadata points at the real repository and the DeepSeek plugin has an extra
|
|
27
|
+
|
|
28
|
+
Homepage, Source, Documentation and Changelog URLs now point at `github.com/ocarinow/ocai` and the docs site. `ocai[deepseek]` installs the DeepSeek plugin and `ocai[all]` includes it. No runtime behavior changed.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ocai-openai
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: OpenAI adapter plugin for ocai (ships ocai.openai into the shared namespace).
|
|
5
|
+
Project-URL: Homepage, https://github.com/ocarinow/ocai
|
|
6
|
+
Project-URL: Source, https://github.com/ocarinow/ocai
|
|
7
|
+
Project-URL: Documentation, https://ocarinow.github.io/ocai/
|
|
8
|
+
Project-URL: Changelog, https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-openai/CHANGELOG.md
|
|
9
|
+
Author: ocai maintainers
|
|
10
|
+
License-Expression: FSL-1.1-ALv2
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Requires-Dist: msgspec>=0.18
|
|
13
|
+
Requires-Dist: ocai<0.2,>=0.1
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# `ocai-openai`
|
|
17
|
+
|
|
18
|
+
The OpenAI **plugin distribution** for [ocai](https://github.com/ocarinow/ocai).
|
|
19
|
+
Distribution name: **`ocai-openai`**; `pip install ocai[openai]` resolves to the
|
|
20
|
+
same wheel. Requires Python 3.13+ and `ocai>=0.1,<0.2`.
|
|
21
|
+
|
|
22
|
+
It ships only `ocai/openai/` into the shared `ocai` PEP 420 namespace,
|
|
23
|
+
organized one subpackage per wire interface:
|
|
24
|
+
|
|
25
|
+
- `ocai.openai.chat_completions` — the `/v1/chat/completions` interface:
|
|
26
|
+
typed wire structs, the codec, contract-censused conversion edges, the
|
|
27
|
+
capability profile, and the chat component. Unary and streaming calls are
|
|
28
|
+
implemented and certified against recorded fixtures.
|
|
29
|
+
- `ocai.openai.responses` — the `/v1/responses` interface. A **stub**: its
|
|
30
|
+
nodes are declared so the interface exists in the graph, but it has no
|
|
31
|
+
edges, codec, or chat component yet. The front door never auto-selects it,
|
|
32
|
+
and pinning `interface="responses"` raises `LookupError` until it lands.
|
|
33
|
+
|
|
34
|
+
The two are sibling interfaces of the one chat capability, not versions of
|
|
35
|
+
each other.
|
|
36
|
+
|
|
37
|
+
## What installing it does
|
|
38
|
+
|
|
39
|
+
Exactly one structural thing: the frozen `ocai.openai.FRAGMENT`
|
|
40
|
+
(a `CatalogFragment`) becomes discoverable through the `ocai.fragments`
|
|
41
|
+
entry-point group, so `ocai.chat.model("openai/<model>", ...)` can resolve the
|
|
42
|
+
provider and the conversion graph gains OpenAI's nodes and edges. Importing
|
|
43
|
+
`ocai.openai` is inert; there is no import-time registration. Applications
|
|
44
|
+
that want an explicit catalog compose it themselves with
|
|
45
|
+
`Catalog.of(ocai.openai.FRAGMENT)`.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from ocai.openai import chat_model
|
|
49
|
+
from ocai.core import ApiKey
|
|
50
|
+
|
|
51
|
+
model = chat_model("gpt-4o", credentials=ApiKey.from_env("OPENAI_API_KEY"))
|
|
52
|
+
result = model.chat(...) # needs ocai[http] and a real key
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`chat_model` is sugar over `ocai.chat.model("openai/<model>", ...)` on a
|
|
56
|
+
catalog built from this fragment, never a separate code path.
|
|
57
|
+
|
|
58
|
+
## Boundaries
|
|
59
|
+
|
|
60
|
+
- No `openai` SDK: ocai owns the wire through `ocai.wire`. This plugin depends
|
|
61
|
+
on `ocai.core` + `ocai.convert` + `ocai.chat` only, never on `ocai.wire` or
|
|
62
|
+
`httpx`, so archived traffic can be re-converted by tooling with no ability
|
|
63
|
+
to make network calls.
|
|
64
|
+
- `msgspec` is used only at the wire-decode boundary.
|
|
65
|
+
- Every conversion edge declares an `EdgeContract` and ships golden fixtures
|
|
66
|
+
in the repository's `spec/`.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
Source-available under FSL-1.1-ALv2 (converting to Apache-2.0 two years after
|
|
71
|
+
each release). ocai is not open source.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# `ocai-openai`
|
|
2
|
+
|
|
3
|
+
The OpenAI **plugin distribution** for [ocai](https://github.com/ocarinow/ocai).
|
|
4
|
+
Distribution name: **`ocai-openai`**; `pip install ocai[openai]` resolves to the
|
|
5
|
+
same wheel. Requires Python 3.13+ and `ocai>=0.1,<0.2`.
|
|
6
|
+
|
|
7
|
+
It ships only `ocai/openai/` into the shared `ocai` PEP 420 namespace,
|
|
8
|
+
organized one subpackage per wire interface:
|
|
9
|
+
|
|
10
|
+
- `ocai.openai.chat_completions` — the `/v1/chat/completions` interface:
|
|
11
|
+
typed wire structs, the codec, contract-censused conversion edges, the
|
|
12
|
+
capability profile, and the chat component. Unary and streaming calls are
|
|
13
|
+
implemented and certified against recorded fixtures.
|
|
14
|
+
- `ocai.openai.responses` — the `/v1/responses` interface. A **stub**: its
|
|
15
|
+
nodes are declared so the interface exists in the graph, but it has no
|
|
16
|
+
edges, codec, or chat component yet. The front door never auto-selects it,
|
|
17
|
+
and pinning `interface="responses"` raises `LookupError` until it lands.
|
|
18
|
+
|
|
19
|
+
The two are sibling interfaces of the one chat capability, not versions of
|
|
20
|
+
each other.
|
|
21
|
+
|
|
22
|
+
## What installing it does
|
|
23
|
+
|
|
24
|
+
Exactly one structural thing: the frozen `ocai.openai.FRAGMENT`
|
|
25
|
+
(a `CatalogFragment`) becomes discoverable through the `ocai.fragments`
|
|
26
|
+
entry-point group, so `ocai.chat.model("openai/<model>", ...)` can resolve the
|
|
27
|
+
provider and the conversion graph gains OpenAI's nodes and edges. Importing
|
|
28
|
+
`ocai.openai` is inert; there is no import-time registration. Applications
|
|
29
|
+
that want an explicit catalog compose it themselves with
|
|
30
|
+
`Catalog.of(ocai.openai.FRAGMENT)`.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from ocai.openai import chat_model
|
|
34
|
+
from ocai.core import ApiKey
|
|
35
|
+
|
|
36
|
+
model = chat_model("gpt-4o", credentials=ApiKey.from_env("OPENAI_API_KEY"))
|
|
37
|
+
result = model.chat(...) # needs ocai[http] and a real key
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`chat_model` is sugar over `ocai.chat.model("openai/<model>", ...)` on a
|
|
41
|
+
catalog built from this fragment, never a separate code path.
|
|
42
|
+
|
|
43
|
+
## Boundaries
|
|
44
|
+
|
|
45
|
+
- No `openai` SDK: ocai owns the wire through `ocai.wire`. This plugin depends
|
|
46
|
+
on `ocai.core` + `ocai.convert` + `ocai.chat` only, never on `ocai.wire` or
|
|
47
|
+
`httpx`, so archived traffic can be re-converted by tooling with no ability
|
|
48
|
+
to make network calls.
|
|
49
|
+
- `msgspec` is used only at the wire-decode boundary.
|
|
50
|
+
- Every conversion edge declares an `EdgeContract` and ships golden fixtures
|
|
51
|
+
in the repository's `spec/`.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
Source-available under FSL-1.1-ALv2 (converting to Apache-2.0 two years after
|
|
56
|
+
each release). ocai is not open source.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ocai-openai"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "OpenAI adapter plugin for ocai (ships ocai.openai into the shared namespace)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "FSL-1.1-ALv2" # source-available (FSL → Apache-2.0 after 2 years)
|
|
12
|
+
authors = [{ name = "ocai maintainers" }]
|
|
13
|
+
# Pin a COMPATIBLE CORE RANGE: every wheel merges into one runtime namespace, so
|
|
14
|
+
# a plugin must match the core public-API (semver) it was built against. The
|
|
15
|
+
# plugin depends on core+convert ONLY — never on ocai.wire or httpx, and it
|
|
16
|
+
# imports NO openai SDK (rule 12). msgspec is the sanctioned decode-boundary
|
|
17
|
+
# tool (rule 11): it decodes the OpenAI wire into typed dialect structs. It
|
|
18
|
+
# stays in the PLUGIN — never in ocai.core.
|
|
19
|
+
dependencies = ["ocai>=0.1,<0.2", "msgspec>=0.18"]
|
|
20
|
+
|
|
21
|
+
# The fragment entry point (`ocai.convert.catalog.ENTRY_POINT_GROUP`): names
|
|
22
|
+
# DATA — the frozen CatalogFragment — never a register() callable that would
|
|
23
|
+
# mutate globals at import time. `Catalog.discover()` composes it opt-in.
|
|
24
|
+
[project.entry-points."ocai.fragments"]
|
|
25
|
+
openai = "ocai.openai:FRAGMENT"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/ocarinow/ocai"
|
|
29
|
+
Source = "https://github.com/ocarinow/ocai"
|
|
30
|
+
Documentation = "https://ocarinow.github.io/ocai/"
|
|
31
|
+
Changelog = "https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-openai/CHANGELOG.md"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
# Ship ONLY ocai/openai/ into the shared `ocai` namespace. Again: no
|
|
35
|
+
# src/ocai/__init__.py (PEP 420).
|
|
36
|
+
packages = ["src/ocai"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build]
|
|
39
|
+
dev-mode-dirs = ["src"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""`ocai.openai` — the OpenAI plugin distribution (`ocai-openai`).
|
|
2
|
+
|
|
3
|
+
Ships in the same `ocai` PEP 420 namespace as the core distribution but as a
|
|
4
|
+
separate dist, organized **one subpackage per interface** (§7):
|
|
5
|
+
|
|
6
|
+
- `ocai.openai.chat_completions` — the `/v1/chat/completions` wire protocol,
|
|
7
|
+
- `ocai.openai.responses` — the `/v1/responses` wire protocol [stub],
|
|
8
|
+
|
|
9
|
+
which are *concurrent sibling interfaces* of the one chat capability, not
|
|
10
|
+
versions of each other. Each subpackage carries its own wire models, codec,
|
|
11
|
+
contract-censused edges, and per-interface `FRAGMENT`.
|
|
12
|
+
|
|
13
|
+
This module composes them into the distribution-level `FRAGMENT` an
|
|
14
|
+
application opts into — `Catalog.of(ocai.openai.FRAGMENT)` — with **no
|
|
15
|
+
import-time registration** anywhere (rule 14; the old process-global
|
|
16
|
+
`register()` side effect is gone). Importing this package is inert. The
|
|
17
|
+
plugin depends on `ocai.core` + `ocai.convert` + `ocai.chat` only — never on
|
|
18
|
+
`ocai.wire` or httpx — so archived traffic can be re-converted by tooling
|
|
19
|
+
with no ability to make network calls.
|
|
20
|
+
|
|
21
|
+
It also re-exports `chat_model`, the sanctioned §3.4 sugar door
|
|
22
|
+
(`from ocai.openai import chat_model; chat_model("gpt-5", credentials=key)`)
|
|
23
|
+
— a thin wrapper over the same fragment + facade machinery as the core
|
|
24
|
+
string door, targeting the `chat_completions` interface today.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from ocai.convert.catalog import CatalogFragment
|
|
30
|
+
from ocai.openai import chat_completions
|
|
31
|
+
from ocai.openai import responses
|
|
32
|
+
from ocai.openai.chat_completions import chat_model
|
|
33
|
+
from ocai.openai.chat_completions.edges import ORIGIN
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"FRAGMENT",
|
|
37
|
+
"chat_completions",
|
|
38
|
+
"chat_model",
|
|
39
|
+
"responses",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# The composed distribution fragment: everything both interfaces publish,
|
|
43
|
+
# under one provenance stamp. Compose the per-interface fragments instead
|
|
44
|
+
# when an application wants only one wire protocol in its catalog.
|
|
45
|
+
FRAGMENT = CatalogFragment(
|
|
46
|
+
origin=ORIGIN,
|
|
47
|
+
nodes=chat_completions.FRAGMENT.nodes + responses.FRAGMENT.nodes,
|
|
48
|
+
edges=chat_completions.FRAGMENT.edges + responses.FRAGMENT.edges,
|
|
49
|
+
transducers=chat_completions.FRAGMENT.transducers + responses.FRAGMENT.transducers,
|
|
50
|
+
profiles=chat_completions.FRAGMENT.profiles + responses.FRAGMENT.profiles,
|
|
51
|
+
traits=chat_completions.FRAGMENT.traits + responses.FRAGMENT.traits,
|
|
52
|
+
components=chat_completions.FRAGMENT.components + responses.FRAGMENT.components,
|
|
53
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""`ocai.openai.chat_completions` — the OpenAI Chat Completions interface.
|
|
2
|
+
|
|
3
|
+
One subpackage per interface (§7): this package is everything ocai knows about
|
|
4
|
+
`POST /v1/chat/completions` as a wire protocol — its msgspec decode-boundary
|
|
5
|
+
models (`wire`), its identity + mapping codec (`codec`), and its declarative
|
|
6
|
+
catalog material (`edges`). The sibling `ocai.openai.responses` package is a
|
|
7
|
+
*concurrent interface* of the same chat capability, not a version of this one.
|
|
8
|
+
|
|
9
|
+
The public surface is `FRAGMENT`: a frozen, provenance-stamped
|
|
10
|
+
`CatalogFragment` an application composes explicitly —
|
|
11
|
+
`Catalog.of(ocai.openai.chat_completions.FRAGMENT, ...)` — with **no
|
|
12
|
+
import-time registration** into any global (rule 14). This package depends on
|
|
13
|
+
`ocai.core` + `ocai.convert` + `ocai.chat` only, never on `ocai.wire`/httpx,
|
|
14
|
+
so offline audit tooling can re-convert archived traffic without the ability
|
|
15
|
+
to make a network call.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from ocai.convert.catalog import Catalog
|
|
23
|
+
from ocai.convert.catalog import CatalogFragment
|
|
24
|
+
from ocai.convert.catalog import Component
|
|
25
|
+
from ocai.core.identity import CHAT
|
|
26
|
+
from ocai.openai.chat_completions.codec import CHAT_COMPLETIONS_CHUNK
|
|
27
|
+
from ocai.openai.chat_completions.codec import CHAT_COMPLETIONS_REQUEST
|
|
28
|
+
from ocai.openai.chat_completions.codec import CHAT_COMPLETIONS_RESPONSE
|
|
29
|
+
from ocai.openai.chat_completions.codec import ChatCompletionsCodec
|
|
30
|
+
from ocai.openai.chat_completions.codec import dialect_traits
|
|
31
|
+
from ocai.openai.chat_completions.edges import EDGES
|
|
32
|
+
from ocai.openai.chat_completions.edges import NODES
|
|
33
|
+
from ocai.openai.chat_completions.edges import ORIGIN
|
|
34
|
+
from ocai.openai.chat_completions.edges import PROFILES
|
|
35
|
+
from ocai.openai.chat_completions.edges import TRANSDUCERS
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from ocai.chat.protocol import ChatModel
|
|
39
|
+
from ocai.core.credentials import Credential
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"CHAT_COMPLETIONS_CHUNK",
|
|
43
|
+
"CHAT_COMPLETIONS_REQUEST",
|
|
44
|
+
"CHAT_COMPLETIONS_RESPONSE",
|
|
45
|
+
"FRAGMENT",
|
|
46
|
+
"ChatCompletionsCodec",
|
|
47
|
+
"chat_model",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# The frozen declarative fragment this interface contributes to a Catalog:
|
|
51
|
+
# its three nodes, its contract-censused edges, its chunk transducer, its
|
|
52
|
+
# capability profile, its hosting traits, and its codec factory (the
|
|
53
|
+
# component the §3.4 front door builds the serving codec from) — all stamped
|
|
54
|
+
# with this distribution's Origin.
|
|
55
|
+
FRAGMENT = CatalogFragment(
|
|
56
|
+
origin=ORIGIN,
|
|
57
|
+
nodes=NODES,
|
|
58
|
+
edges=EDGES,
|
|
59
|
+
transducers=TRANSDUCERS,
|
|
60
|
+
profiles=PROFILES,
|
|
61
|
+
traits=(dialect_traits(),),
|
|
62
|
+
components=(
|
|
63
|
+
Component(
|
|
64
|
+
provider=ChatCompletionsCodec.provider,
|
|
65
|
+
interface=ChatCompletionsCodec.interface,
|
|
66
|
+
capability=CHAT,
|
|
67
|
+
factory=ChatCompletionsCodec,
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def chat_model(
|
|
74
|
+
model_name: str,
|
|
75
|
+
*,
|
|
76
|
+
credentials: Credential,
|
|
77
|
+
) -> ChatModel:
|
|
78
|
+
"""Sanctioned sugar (§3.4 door 2): a first-party OpenAI chat model.
|
|
79
|
+
|
|
80
|
+
A THIN wrapper over the exact same fragment + facade machinery as the
|
|
81
|
+
core string door — never a separate code path: it *is* a call to
|
|
82
|
+
`ocai.chat.model(f"openai/{model_name}", ...)` over a minimal catalog
|
|
83
|
+
built from this interface's `FRAGMENT` (so the front door composes a
|
|
84
|
+
`Direct` host at the OpenAI origin from the fragment's declared traits,
|
|
85
|
+
the `ChatCompletionsCodec` from the fragment's component factory, and an
|
|
86
|
+
`HttpxTransport`). The model name binds to the returned model object
|
|
87
|
+
here, at construction; `ChatRequest`s stay pure conversation and never
|
|
88
|
+
carry it. Power users compose the pieces themselves (any host, any
|
|
89
|
+
transport, richer catalogs); this factory adds nothing they cannot
|
|
90
|
+
reach.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
model_name: The OpenAI model name bound to the returned model
|
|
94
|
+
object (the facade's `EndpointSpec.resource`).
|
|
95
|
+
credentials: The credential applied per request at the driver leaf.
|
|
96
|
+
Required — there is no ambient discovery (rule 13).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A `ChatModel` implementation backed by this interface.
|
|
100
|
+
"""
|
|
101
|
+
# Deferred import (the documented door-2 seam): the plugin stays inert at
|
|
102
|
+
# import time and reaches the wire only through the core front door, at
|
|
103
|
+
# call time — the same composition path, one frame deeper.
|
|
104
|
+
from ocai.chat import model
|
|
105
|
+
|
|
106
|
+
return model(
|
|
107
|
+
f"{ChatCompletionsCodec.provider}/{model_name}",
|
|
108
|
+
credentials=credentials,
|
|
109
|
+
catalog=Catalog.of(FRAGMENT),
|
|
110
|
+
)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Pure client-facing Chat Completions mappings; serving and framing stay outside."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ocai.chat.types import ChatResponse
|
|
10
|
+
from ocai.chat.types import Part
|
|
11
|
+
from ocai.chat.types import PartRefusal
|
|
12
|
+
from ocai.chat.types import PartText
|
|
13
|
+
from ocai.chat.types import PartToolCall
|
|
14
|
+
from ocai.convert.graph import EdgeOutput
|
|
15
|
+
from ocai.core.fidelity import Disposition
|
|
16
|
+
from ocai.core.fidelity import FieldLoss
|
|
17
|
+
from ocai.core.fidelity import SemanticShift
|
|
18
|
+
from ocai.core.finish import FinishInfo
|
|
19
|
+
from ocai.core.ir import IRObject
|
|
20
|
+
from ocai.core.refs import ProviderRef
|
|
21
|
+
from ocai.core.refs import RefScope
|
|
22
|
+
from ocai.core.usage import Usage
|
|
23
|
+
|
|
24
|
+
_FINISH = {
|
|
25
|
+
"complete": "stop",
|
|
26
|
+
"stop_sequence": "stop",
|
|
27
|
+
"max_tokens": "length",
|
|
28
|
+
"tool_call": "tool_calls",
|
|
29
|
+
"content_filter": "content_filter",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ClientMetadata:
|
|
35
|
+
"""Explicit gateway serving identity, never invented provider provenance."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
model: str
|
|
39
|
+
created: int
|
|
40
|
+
|
|
41
|
+
def bind(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
"""Bind serving metadata to a detached response or chunk document."""
|
|
43
|
+
return {**deepcopy(payload), "id": self.id, "model": self.model, "created": self.created}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _loss(
|
|
47
|
+
losses: list[FieldLoss],
|
|
48
|
+
path: str,
|
|
49
|
+
disposition: Disposition = Disposition.DROPPED,
|
|
50
|
+
note: str = "no lane in the qualified client contract",
|
|
51
|
+
) -> None:
|
|
52
|
+
losses.append(FieldLoss(path=path, disposition=disposition, note=note))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _extras(value: IRObject, path: str, losses: list[FieldLoss]) -> None:
|
|
56
|
+
if value.extensions:
|
|
57
|
+
_loss(losses, f"{path}.extensions")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ref_valid(ref: ProviderRef) -> bool:
|
|
61
|
+
return (
|
|
62
|
+
ref.provider == "openai"
|
|
63
|
+
and ref.scope != RefScope.MODEL
|
|
64
|
+
and (ref.interface == "chat_completions" or ref.scope == RefScope.PROVIDER)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def part_evidence(
|
|
69
|
+
part: Part, path: str, losses: list[FieldLoss], *, streaming: bool = False
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Report part metadata with no client lane, including scoped anchors."""
|
|
72
|
+
_extras(part, path, losses)
|
|
73
|
+
if part.anchor is not None:
|
|
74
|
+
_loss(
|
|
75
|
+
losses,
|
|
76
|
+
f"{path}.anchor",
|
|
77
|
+
Disposition.DROPPED if _ref_valid(part.anchor) else Disposition.UNSUPPORTED,
|
|
78
|
+
)
|
|
79
|
+
if part.id is not None:
|
|
80
|
+
_loss(losses, f"{path}.id")
|
|
81
|
+
if part.cache_hint is not None:
|
|
82
|
+
_loss(losses, f"{path}.cache_hint")
|
|
83
|
+
if isinstance(part, PartText) and part.annotations:
|
|
84
|
+
_loss(losses, f"{path}.annotations")
|
|
85
|
+
if isinstance(part, PartToolCall):
|
|
86
|
+
if part.status != "input_complete" and not (streaming and part.status == "input_streaming"):
|
|
87
|
+
_loss(losses, f"{path}.status", Disposition.UNSUPPORTED)
|
|
88
|
+
if part.executed_by != "client":
|
|
89
|
+
_loss(losses, f"{path}.executed_by", Disposition.UNSUPPORTED)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def client_finish(finish: FinishInfo | None, losses: list[FieldLoss]) -> str | None:
|
|
93
|
+
"""Map finish semantics while retaining unknown native values with findings."""
|
|
94
|
+
if finish is None:
|
|
95
|
+
return None
|
|
96
|
+
_extras(finish, "finish", losses)
|
|
97
|
+
if finish.detail:
|
|
98
|
+
_loss(losses, "finish.detail")
|
|
99
|
+
reason = _FINISH.get(finish.reason)
|
|
100
|
+
if reason is None:
|
|
101
|
+
_loss(losses, "finish.reason", Disposition.UNSUPPORTED)
|
|
102
|
+
return finish.native or finish.reason
|
|
103
|
+
if finish.native is not None and finish.native != reason:
|
|
104
|
+
_loss(losses, "finish.native", Disposition.RENAMED, "mapped by canonical finish meaning")
|
|
105
|
+
return reason
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def client_usage(usage: Usage | None, losses: list[FieldLoss]) -> dict[str, Any] | None:
|
|
109
|
+
"""Invert disjoint token buckets without inventing missing measurements."""
|
|
110
|
+
if usage is None:
|
|
111
|
+
return None
|
|
112
|
+
_extras(usage, "usage", losses)
|
|
113
|
+
buckets: dict[str, int] = {}
|
|
114
|
+
for i, item in enumerate(usage.consumptions):
|
|
115
|
+
if (
|
|
116
|
+
item.unit != "tokens"
|
|
117
|
+
or item.modality != "text"
|
|
118
|
+
or item.source not in {"input", "output", "cached", "reasoning"}
|
|
119
|
+
or not isinstance(item.value, int)
|
|
120
|
+
or isinstance(item.value, bool)
|
|
121
|
+
):
|
|
122
|
+
_loss(losses, f"usage.consumptions[{i}]", Disposition.UNSUPPORTED)
|
|
123
|
+
continue
|
|
124
|
+
buckets[item.source] = buckets.get(item.source, 0) + item.value
|
|
125
|
+
result: dict[str, Any] = {}
|
|
126
|
+
if "input" in buckets:
|
|
127
|
+
result["prompt_tokens"] = buckets["input"] + buckets.get("cached", 0)
|
|
128
|
+
if "output" in buckets:
|
|
129
|
+
result["completion_tokens"] = buckets["output"] + buckets.get("reasoning", 0)
|
|
130
|
+
if "prompt_tokens" in result and "completion_tokens" in result:
|
|
131
|
+
result["total_tokens"] = result["prompt_tokens"] + result["completion_tokens"]
|
|
132
|
+
if "cached" in buckets:
|
|
133
|
+
result["prompt_tokens_details"] = {"cached_tokens": buckets["cached"]}
|
|
134
|
+
if "reasoning" in buckets:
|
|
135
|
+
result["completion_tokens_details"] = {"reasoning_tokens": buckets["reasoning"]}
|
|
136
|
+
return result or None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def encode_response(response: ChatResponse) -> EdgeOutput:
|
|
140
|
+
"""Convert canonical response semantics; callers explicitly bind serving metadata."""
|
|
141
|
+
losses: list[FieldLoss] = []
|
|
142
|
+
shifts: list[SemanticShift] = []
|
|
143
|
+
message: dict[str, Any] = {"role": "assistant", "content": None}
|
|
144
|
+
text: list[str] = []
|
|
145
|
+
refusal: list[str] = []
|
|
146
|
+
calls: list[dict[str, Any]] = []
|
|
147
|
+
if response.message.role not in {"model", "assistant"}:
|
|
148
|
+
_loss(losses, "message.role", Disposition.UNSUPPORTED)
|
|
149
|
+
for i, part in enumerate(response.message.parts):
|
|
150
|
+
path = f"message.parts[{i}]"
|
|
151
|
+
part_evidence(part, path, losses)
|
|
152
|
+
if isinstance(part, PartText):
|
|
153
|
+
text.append(part.text)
|
|
154
|
+
elif isinstance(part, PartRefusal):
|
|
155
|
+
refusal.append(part.text)
|
|
156
|
+
elif isinstance(part, PartToolCall):
|
|
157
|
+
calls.append(
|
|
158
|
+
{
|
|
159
|
+
"id": part.call_id,
|
|
160
|
+
"type": "function",
|
|
161
|
+
"function": {"name": part.name, "arguments": part.args_json},
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
_loss(losses, path, Disposition.UNSUPPORTED)
|
|
166
|
+
if text:
|
|
167
|
+
message["content"] = "".join(text)
|
|
168
|
+
if refusal:
|
|
169
|
+
message["refusal"] = "".join(refusal)
|
|
170
|
+
if calls:
|
|
171
|
+
message["tool_calls"] = calls
|
|
172
|
+
if len(response.message.parts) > 1 and (text or refusal):
|
|
173
|
+
shifts.append(
|
|
174
|
+
SemanticShift(
|
|
175
|
+
kind="relocation",
|
|
176
|
+
path="message.parts",
|
|
177
|
+
note="ordered parts grouped into client message lanes",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
_extras(response.message, "message", losses)
|
|
181
|
+
_extras(response, "response", losses)
|
|
182
|
+
result: dict[str, Any] = {
|
|
183
|
+
"object": "chat.completion",
|
|
184
|
+
"choices": [
|
|
185
|
+
{
|
|
186
|
+
"index": 0,
|
|
187
|
+
"message": message,
|
|
188
|
+
"finish_reason": client_finish(response.finish, losses),
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
}
|
|
192
|
+
usage = client_usage(response.usage, losses)
|
|
193
|
+
if usage is not None:
|
|
194
|
+
result["usage"] = usage
|
|
195
|
+
info = response.info
|
|
196
|
+
if info is not None:
|
|
197
|
+
_extras(info, "info", losses)
|
|
198
|
+
if info.id is not None:
|
|
199
|
+
if _ref_valid(info.id):
|
|
200
|
+
result["id"] = info.id.id
|
|
201
|
+
else:
|
|
202
|
+
_loss(losses, "info.id", Disposition.UNSUPPORTED)
|
|
203
|
+
if info.model is not None:
|
|
204
|
+
result["model"] = info.model
|
|
205
|
+
if info.created is not None:
|
|
206
|
+
if info.created.tzinfo is None or info.created.microsecond:
|
|
207
|
+
_loss(losses, "info.created", Disposition.UNSUPPORTED)
|
|
208
|
+
else:
|
|
209
|
+
result["created"] = int(info.created.timestamp())
|
|
210
|
+
return EdgeOutput(result, tuple(losses), tuple(shifts))
|