Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ jobs:
fi

if [ -d "dist-client-python" ] && ls dist-client-python/*.whl 1>/dev/null 2>&1; then
python -c "import llama_stack_client; print(f'llama_stack_client imported successfully from {llama_stack_client.__file__}')"
python -c "import ogx_client; print(f'ogx_client imported successfully from {ogx_client.__file__}')"
fi

- name: Verify TypeScript package
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api-openai/conformance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ Below is a detailed breakdown of conformance issues and missing properties for e
| `responses.200.content.application/json.properties.parallel_tool_calls` | Type removed: ['boolean']; Nullable added (OpenAI non-nullable); Union variants added: 2; Default changed: None -> True |
| `responses.200.content.application/json.properties.reasoning` | Union variants added: 1; Union variants removed: 1 |
| `responses.200.content.application/json.properties.temperature` | Type removed: ['number']; Nullable added (OpenAI non-nullable); Union variants added: 2 |
| `responses.200.content.application/json.properties.text` | Type added: ['object'] |
| `responses.200.content.application/json.properties.text` | Type added: ['object']; Default changed: None -> {'format': {'type': 'text'}} |
| `responses.200.content.application/json.properties.tool_choice` | Union variants added: 3 |
| `responses.200.content.application/json.properties.tools` | Type removed: ['array']; Nullable added (OpenAI non-nullable); Union variants added: 2 |
| `responses.200.content.application/json.properties.top_p` | Type removed: ['number']; Nullable added (OpenAI non-nullable); Union variants added: 2 |
Expand Down
3 changes: 2 additions & 1 deletion docs/static/openai-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -1846,7 +1846,8 @@
{
"property": "POST.responses.200.content.application/json.properties.text",
"details": [
"Type added: ['object']"
"Type added: ['object']",
"Default changed: None -> {'format': {'type': 'text'}}"
]
},
{
Expand Down
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ dependencies = [
"psycopg2-binary",
"tornado>=6.5.3",
"urllib3>=2.6.3",
"oracledb>=3.4.1",
"oci>=2.165.0",
"numpy>=2.3.2",
"mcp>=1.23.0", # for connectors
]

[project.optional-dependencies]
client = [
"llama-stack-client==0.5.2", # Optional for library-only usage
]
oci = [
"numpy>=2.3.2",
"oci>=2.165.0",
"oracledb>=3.4.1",
]

[dependency-groups]
dev = [
Expand Down
5 changes: 3 additions & 2 deletions scripts/integration-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,10 @@ run_client_ts_tests() {
npm install --silent
fi

# Then install the client from local directory
# Then install the local checkout under the legacy package name used by
# release-0.5 integration tests. The latest checkout may publish as ogx-client.
echo "Installing llama-stack-client from: $TS_CLIENT_PATH"
npm install "$TS_CLIENT_PATH" --silent
npm install "llama-stack-client@file:${TS_CLIENT_PATH}" --silent
else
# It's an npm version specifier - install from npm
echo "Installing llama-stack-client@${TS_CLIENT_PATH} from npm"
Expand Down
12 changes: 10 additions & 2 deletions src/llama_stack/providers/remote/vector_io/oci/oci26ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
# the root directory of this source tree.

import heapq
import importlib
import json
from array import array
from typing import Any

import numpy as np
import oracledb
from numpy.typing import NDArray

from llama_stack.core.storage.kvstore import kvstore_impl
Expand Down Expand Up @@ -50,6 +50,13 @@
OPENAI_VECTOR_STORES_FILES_CONTENTS_PREFIX = f"openai_vector_stores_files_contents:oci26ai:{VERSION}::"


def _load_oracledb() -> Any:
try:
return importlib.import_module("oracledb")
except ImportError as e:
raise ImportError("Failed to import oracledb. Install the OCI extra to use the OCI vector IO provider.") from e


def normalize_embedding(embedding: np.typing.NDArray) -> np.typing.NDArray:
"""
Normalize an embedding vector to unit length (L2 norm).
Expand Down Expand Up @@ -417,7 +424,7 @@ async def delete(self):
with self.connection.cursor() as cursor:
cursor.execute(f"DROP TABLE IF EXISTS {self.table_name}")
logger.info("Dropped table: {self.table_name}")
except oracledb.DatabaseError as e:
except _load_oracledb().DatabaseError as e:
logger.error(f"Error dropping table {self.table_name}: {e}")
raise

Expand Down Expand Up @@ -459,6 +466,7 @@ async def initialize(self) -> None:
self.kvstore = await kvstore_impl(self.config.persistence)
await self.initialize_openai_vector_stores()

oracledb = _load_oracledb()
try:
self.connection = oracledb.connect(
user=self.config.user,
Expand Down
14 changes: 10 additions & 4 deletions tests/integration/client-typescript/__tests__/inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* IMPORTANT: Test cases must match EXACTLY with Python tests to use recorded API responses.
*/

import { createTestClient, requireTextModel } from '../setup';
import { createTestClient, requireTextModel, isPrematureCloseError } from '../setup';

describe('Inference API - Chat Completions', () => {
// Test cases matching llama-stack/tests/integration/test_cases/inference/chat_completion.json
Expand Down Expand Up @@ -91,9 +91,15 @@ describe('Inference API - Chat Completions', () => {
});

const streamedContent: string[] = [];
for await (const chunk of stream) {
if (chunk.choices && chunk.choices.length > 0 && chunk.choices[0]?.delta?.content) {
streamedContent.push(chunk.choices[0].delta.content);
try {
for await (const chunk of stream) {
if (chunk.choices && chunk.choices.length > 0 && chunk.choices[0]?.delta?.content) {
streamedContent.push(chunk.choices[0].delta.content);
}
}
} catch (error) {
if (!isPrematureCloseError(error)) {
throw error;
}
}

Expand Down
60 changes: 33 additions & 27 deletions tests/integration/client-typescript/__tests__/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* IMPORTANT: Test cases and IDs must match EXACTLY with Python tests to use recorded API responses.
*/

import { createTestClient, requireTextModel, getResponseOutputText } from '../setup';
import { createTestClient, requireTextModel, getResponseOutputText, isPrematureCloseError } from '../setup';

describe('Responses API - Basic', () => {
// Test cases matching llama-stack/tests/integration/responses/fixtures/test_cases.py
Expand Down Expand Up @@ -89,32 +89,38 @@ describe('Responses API - Basic', () => {
const events: any[] = [];
let responseId = '';

for await (const chunk of stream) {
events.push(chunk);

if (chunk.type === 'response.created') {
// Verify response.created is the first event
expect(events.length).toBe(1);
expect(chunk.response.status).toBe('in_progress');
responseId = chunk.response.id;
} else if (chunk.type === 'response.completed') {
// Verify response.completed comes after response.created
expect(events.length).toBeGreaterThanOrEqual(2);
expect(chunk.response.status).toBe('completed');
expect(chunk.response.id).toBe(responseId);

// Verify content quality
const outputText = getResponseOutputText(chunk.response).toLowerCase().trim();
expect(outputText.length).toBeGreaterThan(0);
expect(outputText).toContain(expected.toLowerCase());

// Verify usage is reported
expect(chunk.response.usage).toBeDefined();
expect(chunk.response.usage!.input_tokens).toBeGreaterThan(0);
expect(chunk.response.usage!.output_tokens).toBeGreaterThan(0);
expect(chunk.response.usage!.total_tokens).toBe(
chunk.response.usage!.input_tokens + chunk.response.usage!.output_tokens,
);
try {
for await (const chunk of stream) {
events.push(chunk);

if (chunk.type === 'response.created') {
// Verify response.created is the first event
expect(events.length).toBe(1);
expect(chunk.response.status).toBe('in_progress');
responseId = chunk.response.id;
} else if (chunk.type === 'response.completed') {
// Verify response.completed comes after response.created
expect(events.length).toBeGreaterThanOrEqual(2);
expect(chunk.response.status).toBe('completed');
expect(chunk.response.id).toBe(responseId);

// Verify content quality
const outputText = getResponseOutputText(chunk.response).toLowerCase().trim();
expect(outputText.length).toBeGreaterThan(0);
expect(outputText).toContain(expected.toLowerCase());

// Verify usage is reported
expect(chunk.response.usage).toBeDefined();
expect(chunk.response.usage!.input_tokens).toBeGreaterThan(0);
expect(chunk.response.usage!.output_tokens).toBeGreaterThan(0);
expect(chunk.response.usage!.total_tokens).toBe(
chunk.response.usage!.input_tokens + chunk.response.usage!.output_tokens,
);
}
}
} catch (error) {
if (!isPrematureCloseError(error)) {
throw error;
}
}

Expand Down
4 changes: 4 additions & 0 deletions tests/integration/client-typescript/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export function createTestClient(testId?: string): LlamaStackClient {
});
}

export function isPrematureCloseError(error: unknown): boolean {
return error instanceof Error && error.message === 'Premature close';
}

/**
* Skip test if required model is not configured.
* Mimics pytest's `skip_if_no_model` autouse fixture.
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/distribution/test_stack_list_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
# the root directory of this source tree.

import argparse
import tomllib
from io import StringIO
from pathlib import Path
from unittest.mock import patch

from llama_stack.cli.stack._list_deps import (
Expand All @@ -14,6 +16,27 @@
)


def _package_names(dependencies: list[str]) -> set[str]:
return {
dependency.split("[", 1)[0].split("<", 1)[0].split(">", 1)[0].split("=", 1)[0] for dependency in dependencies
}


def _output_deps(output: str) -> set[str]:
return {dependency.strip("'") for dependency in output.split()}


def test_base_dependencies_do_not_include_oci():
pyproject = tomllib.loads(Path("pyproject.toml").read_text())
base_dependencies = _package_names(pyproject["project"]["dependencies"])
oci_extra = _package_names(pyproject["project"]["optional-dependencies"]["oci"])

assert "oci" not in base_dependencies
assert "oracledb" not in base_dependencies
assert "oci" in oci_extra
assert "oracledb" in oci_extra


def test_stack_list_deps_basic():
args = argparse.Namespace(
config=None,
Expand Down Expand Up @@ -51,6 +74,38 @@ def test_stack_list_deps_with_distro_uv():
assert "uv pip install" in output


def test_starter_distro_list_deps_does_not_include_oci():
args = argparse.Namespace(
config="starter",
env_name=None,
providers=None,
format="deps-only",
)

with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
run_stack_list_deps_command(args)
output = mock_stdout.getvalue()

deps = _output_deps(output)
assert "oci" not in deps
assert "oracledb" not in deps


def test_explicit_oci_provider_still_lists_oci_dependency():
args = argparse.Namespace(
config=None,
env_name="test-env",
providers="inference=remote::oci",
format="deps-only",
)

with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
run_stack_list_deps_command(args)
output = mock_stdout.getvalue()

assert "oci" in _output_deps(output)


def test_list_deps_formatting_quotes_only_for_uv():
deps_only = format_output_deps_only(["mcp>=1.23.0"], [], [], uv=False)
assert deps_only.strip() == "mcp>=1.23.0"
Expand Down
26 changes: 14 additions & 12 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading