Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/interfaze/__init__.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
"""Official Interfaze SDK — a typed wrapper over the OpenAI SDK."""
"""Official Interfaze SDK for Python."""

from __future__ import annotations

from openai import (
Expand Down
8 changes: 6 additions & 2 deletions src/interfaze/_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,7 +67,9 @@ def __init__(
self.openai = OpenAI(
api_key=_resolve_key(api_key),
base_url=base_url or INTERFAZE_BASE_URL,
default_headers=_build_headers(default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key),
default_headers=_build_headers(
default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key
),
**kwargs,
)
self.chat = Chat(self.openai)
Expand All@@ -93,7 +95,9 @@ def __init__(
self.openai = AsyncOpenAI(
api_key=_resolve_key(api_key),
base_url=base_url or INTERFAZE_BASE_URL,
default_headers=_build_headers(default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key),
default_headers=_build_headers(
default_headers, show_additional_info, bypass_moe, bypass_cache, admin_key
),
**kwargs,
)
self.chat = AsyncChat(self.openai)
Expand Down
20 changes: 17 additions & 3 deletions src/interfaze/_constants.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,23 @@

# Guardrail categories (ALL enables everything).
GUARD_CODES = (
"S1", "S2", "S3", "S4", "S5", "S6", "S7",
"S8", "S9", "S10", "S11", "S12", "S13", "S14",
"S1_IMAGE", "S12_IMAGE", "S15_IMAGE",
"S1",
"S2",
"S3",
"S4",
"S5",
"S6",
"S7",
"S8",
"S9",
"S10",
"S11",
"S12",
"S13",
"S14",
"S1_IMAGE",
"S12_IMAGE",
"S15_IMAGE",
"ALL",
)

Expand Down
37 changes: 29 additions & 8 deletions src/interfaze/_inputs.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,14 +10,35 @@
BytesLike = Union[bytes, bytearray]

_EXT_MIME = {
"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "webp": "image/webp",
"gif": "image/gif", "bmp": "image/bmp", "heic": "image/heic", "heif": "image/heif",
"pdf": "application/pdf", "csv": "text/csv", "tsv": "text/tab-separated-values",
"xml": "application/xml", "json": "application/json", "txt": "text/plain",
"md": "text/markdown", "markdown": "text/markdown", "yaml": "application/yaml", "yml": "application/yaml",
"wav": "audio/wav", "mp3": "audio/mpeg", "m4a": "audio/mp4", "ogg": "audio/ogg", "flac": "audio/flac",
"mp4": "video/mp4", "mov": "video/quicktime", "webm": "video/webm", "avi": "video/x-msvideo",
"mkv": "video/x-matroska", "3gp": "video/3gpp",
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"webp": "image/webp",
"gif": "image/gif",
"bmp": "image/bmp",
"heic": "image/heic",
"heif": "image/heif",
"pdf": "application/pdf",
"csv": "text/csv",
"tsv": "text/tab-separated-values",
"xml": "application/xml",
"json": "application/json",
"txt": "text/plain",
"md": "text/markdown",
"markdown": "text/markdown",
"yaml": "application/yaml",
"yml": "application/yaml",
"wav": "audio/wav",
"mp3": "audio/mpeg",
"m4a": "audio/mp4",
"ogg": "audio/ogg",
"flac": "audio/flac",
"mp4": "video/mp4",
"mov": "video/quicktime",
"webm": "video/webm",
"avi": "video/x-msvideo",
"mkv": "video/x-matroska",
"3gp": "video/3gpp",
}


Expand Down
8 changes: 6 additions & 2 deletions src/interfaze/_tasks.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,7 +60,9 @@ def translate(self, text: str, *, to: str) -> Any:
return self._run("translate", f"Translate the following into {to}:\n\n{text}")

def forecast(self, csv_source: str, *, periods: int = 10, unit: str = "days") -> Any:
r = self._c.create(messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}])
r = self._c.create(
messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}]
)
for p in r.precontext or []:
if p.name == "forecast":
return p.result
Expand DownExpand Up@@ -99,7 +101,9 @@ async def translate(self, text: str, *, to: str) -> Any:
return await self._run("translate", f"Translate the following into {to}:\n\n{text}")

async def forecast(self, csv_source: str, *, periods: int = 10, unit: str = "days") -> Any:
r = await self._c.create(messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}])
r = await self._c.create(
messages=[{"role": "user", "content": _forecast_prompt(csv_source, periods, unit)}]
)
for p in r.precontext or []:
if p.name == "forecast":
return p.result
Expand Down
38 changes: 22 additions & 16 deletions src/interfaze/_types.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
from typing import Any, List, Literal, Optional

from openai.types.chat import ChatCompletion
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

TaskName = Literal[
"ocr",
Expand All@@ -16,35 +16,41 @@
]

GuardCode = Literal[
"S1", "S2", "S3", "S4", "S5", "S6", "S7",
"S8", "S9", "S10", "S11", "S12", "S13", "S14",
"S1_IMAGE", "S12_IMAGE", "S15_IMAGE",
"S1",
"S2",
"S3",
"S4",
"S5",
"S6",
"S7",
"S8",
"S9",
"S10",
"S11",
"S12",
"S13",
"S14",
"S1_IMAGE",
"S12_IMAGE",
"S15_IMAGE",
"ALL",
]

# Interfaze accepts these reasoning levels (wider than the OpenAI enum).
ReasoningEffort = Literal["minimal", "low", "medium", "high", "on", "off", "auto"]


class Precontext(BaseModel):
"""One internal task's raw output, surfaced in ``response.precontext``."""
"""One internal task's output; lenient since raw tool-call entries omit name/result."""

name: str
model_config = ConfigDict(extra="allow")
name: Optional[str] = None
result: Any = None


class InterfazeChatCompletion(ChatCompletion):
"""A chat completion extended with the fields Interfaze adds.

openai-python already preserves these as pydantic extras (``extra='allow'``); this
subclass merely gives them declared, typed attributes for IDE/type-checker support.
"""
"""ChatCompletion plus Interfaze extras: precontext, reasoning, vcache, debug."""

precontext: Optional[List[Precontext]] = None
"""Present when internal tools ran (OCR / web search / scrape / STT / forecast / …)."""
reasoning: Optional[str] = None
"""Reasoning text — present with ``reasoning_effort='high'`` and no schema."""
vcache: bool = False
"""Whether the semantic cache was hit."""
debug: Optional[Any] = None
"""Admin-only debug payload (requires ``admin_key``)."""
28 changes: 22 additions & 6 deletions tests/conftest.py
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
"""Test fixtures mirroring REAL Interfaze wire responses (observed live).
"""Test fixtures mirroring Interfaze wire responses (observed live).

Kept faithful to the actual shapes: `vcache` always present, `precontext` a list of
{name,result}, task content = {name,result} JSON, json_object content ```json-fenced,
Kept faithful to the actual shapes: `vcache` always present,
`precontext` a list of {name,result},
task content = {name,result} JSON,
json_object content ```json-fenced,
stream deltas role-less.
"""

Expand All@@ -15,7 +17,11 @@

CHAT_URL = "https://api.interfaze.ai/v1/chat/completions"

_USAGE = {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8}
_USAGE = {
"prompt_tokens": 5,
"completion_tokens": 3,
"total_tokens": 8,
}


def completion(
Expand All@@ -40,10 +46,20 @@ def completion(

BASIC = completion("Hi!")
PRECONTEXT = completion(
"Total: $12.34", precontext=[{"name": "ocr", "result": {"extracted_text": "Walmart ... TOTAL 12.34"}}]
"Total: $12.34",
precontext=[{"name": "ocr", "result": {"extracted_text": "Walmart ... TOTAL 12.34"}}],
)
# A task entry plus a raw tool-call entry (server-appended on tool/run_code turns).
MIXED_PRECONTEXT = completion(
"Ran the code.",
precontext=[
{"name": "ocr", "result": {"extracted_text": "x"}},
{"toolCallId": "call_1", "toolName": "run_code", "input": {"code": "print(1)"}},
],
)
REASONING = completion(
"The sky is blue because...", reasoning="Rayleigh scattering means shorter wavelengths..."
"The sky is blue because...",
reasoning="Rayleigh scattering means shorter wavelengths...",
)
JSON_OBJECT = completion('```json\n{\n "city": "Tokyo",\n "temp_c": 21\n}\n```')
TASK_OCR = completion('{"name": "ocr", "result": {"extracted_text": "See back of receipt", "width": 800}}')
Expand Down
15 changes: 15 additions & 0 deletions tests/test_chat.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,7 @@
from conftest import (
BASIC,
JSON_OBJECT,
MIXED_PRECONTEXT,
PRECONTEXT,
REASONING,
TASK_OCR,
Expand DownExpand Up@@ -106,6 +107,19 @@ def test_precontext_and_vcache_typed():
assert isinstance(r.vcache, bool)


@respx.mock
def test_precontext_tolerates_raw_toolcall_entries():
# raw tool-call entries in precontext must not raise.
mock_json(MIXED_PRECONTEXT)
r = Interfaze(api_key="t").chat.completions.create(messages=[{"role": "user", "content": "run code"}])
assert isinstance(r, InterfazeChatCompletion)
assert r.precontext is not None
assert len(r.precontext) == 2
assert r.precontext[0].name == "ocr" and r.precontext[0].result == {"extracted_text": "x"}
assert r.precontext[1].name is None
assert (r.precontext[1].model_extra or {}).get("toolName") == "run_code"


@respx.mock
def test_reasoning_typed():
mock_json(REASONING)
Expand All@@ -120,6 +134,7 @@ def test_json_object_fence_stripped():
messages=[{"role": "user", "content": "x"}], response_format={"type": "json_object"}
)
content = r.choices[0].message.content
assert content is not None
assert not content.strip().startswith("```")
assert json.loads(content)["city"] == "Tokyo"

Expand Down