From df566eb0ea60cfed62defbcf144b686514d049f1 Mon Sep 17 00:00:00 2001 From: Varun Shenoy Date: Wed, 11 Mar 2026 13:22:59 -0700 Subject: [PATCH 1/4] [Feature] Adding support for oci-genai-auth-python. Signed-off-by: Varun Shenoy --- .github/workflows/publish-to-pypi.yml | 46 ++ .github/workflows/run-all-check.yaml | 39 ++ .gitignore | 87 ++++ CONTRIBUTING.md | 2 - LICENSE.txt | 2 +- Makefile | 69 +++ README.md | 454 ++++++++++++++++++ THIRD_PARTY_LICENSES.txt | 262 ++++++++++ examples/__init__.py | 2 + examples/anthropic/basic_messages.py | 23 + examples/anthropic/basic_messages_api_key.py | 27 ++ examples/anthropic/basic_messages_async.py | 26 + .../anthropic/messages_context_management.py | 56 +++ examples/anthropic/messages_image_text.py | 42 ++ examples/anthropic/messages_memory.py | 175 +++++++ examples/anthropic/messages_speed_fast.py | 36 ++ examples/anthropic/messages_streaming.py | 33 ++ .../messages_tool_search_client_side.py | 106 ++++ examples/anthropic/messages_web_fetch.py | 42 ++ examples/anthropic/messages_web_search.py | 41 ++ examples/common.py | 153 ++++++ examples/fc_tools.py | 119 +++++ examples/google/generate_content.py | 20 + examples/google/generate_content_api_key.py | 25 + examples/google/generate_content_async.py | 23 + examples/google/generate_content_streaming.py | 28 ++ examples/google/generate_images.py | 25 + examples/openai/__init__.py | 2 + examples/openai/basic_agents_example.py | 33 ++ .../chat_completion/basic_chat_completion.py | 21 + .../basic_chat_completion_api_key.py | 34 ++ .../streaming_chat_completion.py | 30 ++ .../tool_call_chat_completion.py | 87 ++++ .../openai/conversation_items/create_items.py | 25 + .../openai/conversation_items/delete_item.py | 14 + .../openai/conversation_items/list_items.py | 11 + .../conversation_items/retrieve_item.py | 14 + .../conversations/create_conversation.py | 13 + .../conversations/delete_conversation.py | 11 + .../conversations/retrieve_conversation.py | 13 + .../conversations/update_conversation.py | 13 + examples/openai/create_responses_mcp.py | 31 ++ examples/openai/create_responses_multiturn.py | 40 ++ examples/openai/create_responses_websearch.py | 28 ++ .../create_responses_with_conversation_id.py | 36 ++ .../create_response_fc_ parallel_tool.py | 32 ++ .../openai/function/create_responses_fc.py | 77 +++ .../mcp/create_response_approval_flow.py | 38 ++ .../openai/mcp/create_response_two_mcp.py | 34 ++ examples/openai/mcp/create_responses_mcp.py | 38 ++ .../openai/mcp/create_responses_mcp_auth.py | 27 ++ examples/openai/multitools/mcp_function.py | 45 ++ examples/openai/multitools/mcp_websearch.py | 32 ++ .../oci_openai_chat_completions_example.py | 44 ++ .../openai/oci_openai_responses_example.py | 23 + examples/openai/responses/Cat.jpg | Bin 0 -> 166820 bytes examples/openai/responses/__init__.py | 2 + examples/openai/responses/create_response.py | 18 + .../responses/create_response_streaming.py | 25 + .../create_response_with_image_input.py | 34 ++ examples/openai/responses/delete_response.py | 13 + examples/openai/responses/get_response.py | 13 + examples/openai/responses/list_input_items.py | 13 + .../websearch/create_responses_websearch.py | 23 + .../create_responses_websearch_source.py | 27 ++ mypy.ini | 6 + pyproject.toml | 118 +++++ src/oci_genai_auth/__about__.py | 4 + src/oci_genai_auth/__init__.py | 20 + src/oci_genai_auth/auth.py | 383 +++++++++++++++ tests/__init__.py | 2 + tests/conftest.py | 19 + tests/test_auth.py | 146 ++++++ 73 files changed, 3672 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 .github/workflows/run-all-check.yaml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 THIRD_PARTY_LICENSES.txt create mode 100644 examples/__init__.py create mode 100644 examples/anthropic/basic_messages.py create mode 100644 examples/anthropic/basic_messages_api_key.py create mode 100644 examples/anthropic/basic_messages_async.py create mode 100644 examples/anthropic/messages_context_management.py create mode 100644 examples/anthropic/messages_image_text.py create mode 100644 examples/anthropic/messages_memory.py create mode 100644 examples/anthropic/messages_speed_fast.py create mode 100644 examples/anthropic/messages_streaming.py create mode 100644 examples/anthropic/messages_tool_search_client_side.py create mode 100644 examples/anthropic/messages_web_fetch.py create mode 100644 examples/anthropic/messages_web_search.py create mode 100644 examples/common.py create mode 100644 examples/fc_tools.py create mode 100644 examples/google/generate_content.py create mode 100644 examples/google/generate_content_api_key.py create mode 100644 examples/google/generate_content_async.py create mode 100644 examples/google/generate_content_streaming.py create mode 100644 examples/google/generate_images.py create mode 100644 examples/openai/__init__.py create mode 100644 examples/openai/basic_agents_example.py create mode 100644 examples/openai/chat_completion/basic_chat_completion.py create mode 100644 examples/openai/chat_completion/basic_chat_completion_api_key.py create mode 100644 examples/openai/chat_completion/streaming_chat_completion.py create mode 100644 examples/openai/chat_completion/tool_call_chat_completion.py create mode 100644 examples/openai/conversation_items/create_items.py create mode 100644 examples/openai/conversation_items/delete_item.py create mode 100644 examples/openai/conversation_items/list_items.py create mode 100644 examples/openai/conversation_items/retrieve_item.py create mode 100644 examples/openai/conversations/create_conversation.py create mode 100644 examples/openai/conversations/delete_conversation.py create mode 100644 examples/openai/conversations/retrieve_conversation.py create mode 100644 examples/openai/conversations/update_conversation.py create mode 100644 examples/openai/create_responses_mcp.py create mode 100644 examples/openai/create_responses_multiturn.py create mode 100644 examples/openai/create_responses_websearch.py create mode 100644 examples/openai/create_responses_with_conversation_id.py create mode 100644 examples/openai/function/create_response_fc_ parallel_tool.py create mode 100644 examples/openai/function/create_responses_fc.py create mode 100644 examples/openai/mcp/create_response_approval_flow.py create mode 100644 examples/openai/mcp/create_response_two_mcp.py create mode 100644 examples/openai/mcp/create_responses_mcp.py create mode 100644 examples/openai/mcp/create_responses_mcp_auth.py create mode 100644 examples/openai/multitools/mcp_function.py create mode 100644 examples/openai/multitools/mcp_websearch.py create mode 100755 examples/openai/oci_openai_chat_completions_example.py create mode 100644 examples/openai/oci_openai_responses_example.py create mode 100644 examples/openai/responses/Cat.jpg create mode 100644 examples/openai/responses/__init__.py create mode 100644 examples/openai/responses/create_response.py create mode 100644 examples/openai/responses/create_response_streaming.py create mode 100644 examples/openai/responses/create_response_with_image_input.py create mode 100644 examples/openai/responses/delete_response.py create mode 100644 examples/openai/responses/get_response.py create mode 100644 examples/openai/responses/list_input_items.py create mode 100644 examples/openai/websearch/create_responses_websearch.py create mode 100644 examples/openai/websearch/create_responses_websearch_source.py create mode 100644 mypy.ini create mode 100644 pyproject.toml create mode 100644 src/oci_genai_auth/__about__.py create mode 100644 src/oci_genai_auth/__init__.py create mode 100644 src/oci_genai_auth/auth.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_auth.py diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..12c7522 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,46 @@ +name: Publish Python Package + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +jobs: + release-build: + runs-on: ubuntu-latest + # PyPI use trusted publisher + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install uv + uv venv + make dev + - name: Build + run: | + make build + - name: Validate + run: | + WHEEL=$(ls dist/*.whl | head -n 1) + python -m pip install "${WHEEL}[openai,google,anthropic]" + python -c "from oci_genai_auth.openai import OciOpenAI; from oci_genai_auth.google import OciGoogleGenAI; from oci_genai_auth.anthropic import OciAnthropic; import oci_genai_auth;" + # - name: Publish to Test PyPI + # run: | + # python -m pip install twine + # twine check dist/* + # twine upload --verbose -r testpypi dist/* + - name: Publish to PyPI + run: | + python -m pip install twine + twine check dist/* + twine upload --verbose dist/* diff --git a/.github/workflows/run-all-check.yaml b/.github/workflows/run-all-check.yaml new file mode 100644 index 0000000..293f7a9 --- /dev/null +++ b/.github/workflows/run-all-check.yaml @@ -0,0 +1,39 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Unit test, format and lint check + +on: + workflow_dispatch: + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install uv + uv venv + make dev + - name: Format and Lint + run: | + make check + - name: Test with pytest + run: | + make test + - name: Build + run: | + make build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a46842d --- /dev/null +++ b/.gitignore @@ -0,0 +1,87 @@ +# Mac +.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ +*.iml + +# Visual Studio Code +.vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Demo folder +.demo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85ab22a..637430b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,3 @@ -*Detailed instructions on how to contribute to the project, if applicable. Must include section about Oracle Contributor Agreement with link and instructions* - # Contributing to this repository We welcome your contributions! There are multiple ways to contribute. diff --git a/LICENSE.txt b/LICENSE.txt index bb91ea7..92e1920 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2026 Oracle and/or its affiliates. +Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. The Universal Permissive License (UPL), Version 1.0 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..89a7b74 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# Define the directory containing the source code +SRC_DIR := ./src +TEST_DIR := ./tests +EXAMPLE_DIR := ./examples + +# Optional install extras via `make install ` or `make build ` +ifneq (,$(filter install build,$(MAKECMDGOALS))) +EXTRAS := $(filter-out install build,$(MAKECMDGOALS)) +comma := , +empty := +space := $(empty) $(empty) +EXTRA_LIST := $(subst $(space),$(comma),$(strip $(EXTRAS))) +endif + +.PHONY: all +all: test lint build + +##@ General + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: install +install: ## Install project dependencies. Example: `make install openai` +ifneq ($(strip $(EXTRA_LIST)),) + uv pip install --editable ".[${EXTRA_LIST}]" +else + uv pip install --editable . +endif + +ifneq ($(strip $(EXTRAS)),) +.PHONY: $(EXTRAS) +$(EXTRAS): + @: +endif + +.PHONY: dev +dev: ## Install development dependencies. + uv pip install ".[dev]" + +.PHONY: test +test: ## Run tests. + uv run --no-project --no-reinstall pytest $(TEST_DIR) --cov --cov-config=.coveragerc -vv -s + +.PHONY: clean +clean: ## Remove build artifacts. + rm -rf build dist *.egg-info .pytest_cache .coverage + +.PHONY: format +format: ## Format code using ruff. + uv run --no-project --no-reinstall isort $(SRC_DIR) $(TEST_DIR) $(EXAMPLE_DIR) + uv run --no-project --no-reinstall ruff format $(SRC_DIR) $(TEST_DIR) $(EXAMPLE_DIR); uv run --no-project --no-reinstall ruff check --fix $(SRC_DIR) $(TEST_DIR) $(EXAMPLE_DIR) + +.PHONY: lint +lint: ## Run linters using ruff. + uv run --no-project --no-reinstall ruff format --diff $(SRC_DIR) $(TEST_DIR) + uv run --no-project --no-reinstall mypy $(SRC_DIR) $(TEST_DIR) + +.PHONY: check +check: format lint ## Run format and lint. + +##@ Build + +.PHONY: build +build: ## Build the application. + uv build diff --git a/README.md b/README.md index 73e8102..02e8a55 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,457 @@ Copyright (c) 2026 Oracle and/or its affiliates. Released under the Universal Permissive License v1.0 as shown at . +======= +# oci-genai-auth + +[![PyPI - Version](https://img.shields.io/pypi/v/oci-genai-auth.svg)](https://pypi.org/project/oci-genai-auth) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oci-genai-auth.svg)](https://pypi.org/project/oci-genai-auth) + +The **OCI GenAI Auth** Python library provides secure and convenient access to the OpenAI-compatible REST API hosted by **OCI Generative AI Service** and **OCI Data Science Model Deployment** Service. + +--- + +## Table of Contents + +- [oci-genai-auth](#oci-genai-auth) + - [Table of Contents](#table-of-contents) + - [Before You Start](#before-you-start) + - [Installation](#installation) + - [Examples](#examples) + - [OCI Generative AI](#oci-generative-ai) + - [Using the Native OpenAI Client](#using-the-native-openai-client) + - [Using with Langchain](#using-with-langchain-openai) + - [Google Gen AI (Gemini)](#google-gen-ai-gemini) + - [Using the Native Google Gen AI Client](#using-the-native-google-gen-ai-client) + - [Generate Images](#generate-images) + - [Async Generate Content](#async-generate-content) + - [Anthropic](#anthropic) + - [Using the Native Anthropic Client](#using-the-native-anthropic-client) + - [Async Messages](#async-messages) + - [OCI Data Science Model Deployment](#oci-data-science-model-deployment) + - [Using the Native OpenAI Client](#using-the-native-openai-client-1) + - [Signers](#signers) + - [Contributing](#contributing) + - [Security](#security) + - [License](#license) + +--- + +## Before you start + +**Important!** + +This package provides OCI request-signing helpers for `httpx`. It does not ship OCI-specific SDK clients. Use the native OpenAI SDK with a custom `httpx` client that applies OCI signing. + +Before you start using this package, determine if this is the right option for you. + +If you are looking for a seamless way to port your code from an OpenAI compatible endpoint to OCI Generative AI endpoint, and you are currently using OpenAI-style API keys, you might want to use [OCI Generative AI API Keys](https://docs.oracle.com/en-us/iaas/Content/generative-ai/api-keys.htm) instead. + +With OCI Generative AI API Keys, use the native `openai` SDK like before. Just update the `base_url`, create API keys in your OCI console, insure the policy granting the key access to generative AI services is present and you are good to go. + +- Create an API key in Console: **Generative AI** -> **API Keys** +- Create a security policy: **Identity & Security** -> **Policies** + +To authorize a specific API Key +``` +allow any-user to use generative-ai-family in compartment where ALL { request.principal.type='generativeaiapikey', request.principal.id='ocid1.generativeaiapikey.oc1.us-chicago-1....' } +``` + +To authorize any API Key +``` +allow any-user to use generative-ai-family in compartment where ALL { request.principal.type='generativeaiapikey' } +``` + +- Update the `base_url` in your code: + +```python +from openai import OpenAI +import os + +API_KEY=os.getenv("OPENAI_API_KEY") + +print(API_KEY) + +client = OpenAI( + api_key=API_KEY, + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1" +) + +# Responses API +response = client.responses.create( + model="openai.gpt-oss-120b", + # model="xai.grok-3", + # meta models are not supported with the Responses API + input="Write a one-sentence bedtime story about a unicorn." +) +print(response) + +# Completion API +response = client.chat.completions.create( + # model="openai.gpt-oss-120b", + # model="meta.llama-3.3-70b-instruct", + model="xai.grok-3", + messages=[{ + "role": "user", + "content": "Write a one-sentence bedtime story about a unicorn." + } + ] +) +print(response) +``` + + +API Keys offer a seamless transition from code using the openai SDK, and allow usage in 3rd party code or services that don't offer an override of the http client. + +However, if authentication at the user, compute instance, resource or workload level (OKE pods) is preferred, this package is for you. + +It offers the same compatibility with the `openai` SDK, but requires patching the http client. See the following instruction on how to use it. + +## Installation + +```console +pip install oci-genai-auth +``` + +Install the SDKs you plan to use separately, for example: + +```console +pip install openai +pip install anthropic +pip install google-genai +``` + +--- + +## Examples + +### OCI Generative AI + +Notes: + +- **Cohere models do not support OpenAI-compatible API** +- **OCI Generative AI requires the OpenAI SDK base URL to end with `/openai/v1`** + +#### Using the Native OpenAI Client + +```python + +import httpx +from openai import OpenAI +from oci_genai_auth import OciSessionAuth + +# Example for OCI Generative AI endpoint +client = OpenAI( + api_key="not-used", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", + http_client=httpx.Client( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), +) + +completion = client.chat.completions.create( + model="", + messages=[ + { + "role": "user", + "content": "How do I output all files in a directory using Python?", + }, + ], +) +print(completion.model_dump_json()) + +``` + +If you only need the auth helpers (no OCI-specific OpenAI shim client), you can import them from +`oci_genai_auth` which only exposes the signer classes used by `httpx`. + +#### Using with langchain-openai + +```python +from langchain_openai import ChatOpenAI +import httpx +from oci_genai_auth import OciUserPrincipalAuth + + +llm = ChatOpenAI( + model="", # for example "xai.grok-4-fast-reasoning" + api_key="not-used", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", + http_client=httpx.Client( + auth=OciUserPrincipalAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), + # use_responses_api=True + # stream_usage=True, + # temperature=None, + # max_tokens=None, + # timeout=None, + # reasoning_effort="low", + # max_retries=2, + # other params... +) + +messages = [ + ( + "system", + "You are a helpful assistant that translates English to French. Translate the user sentence.", + ), + ("human", "I love programming."), +] +ai_msg = llm.invoke(messages) +print(ai_msg) +``` + +--- + +### Google Gen AI (Gemini) + +#### Using the Native Google Gen AI Client + +```python +import httpx +from google import genai +from oci_genai_auth import OciSessionAuth + +client = genai.Client( + api_key="not-used", + http_options={ + "base_url": "https://", + "httpx_client": httpx.Client( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), + }, +) + +response = client.models.generate_content( + model="gemini-2.0-flash-001", + contents="Write a one-sentence bedtime story about a unicorn.", +) +print(response) +``` + +#### Generate Images + +```python +import httpx +from google import genai +from oci_genai_auth import OciSessionAuth + +client = genai.Client( + api_key="not-used", + http_options={ + "base_url": "https://", + "httpx_client": httpx.Client( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), + }, +) + +response = client.models.generate_images( + model="imagen-3.0-generate-002", + prompt="A poster of a mythical dragon in a neon city.", +) +print(response) +``` + +#### Async Generate Content + +```python +import asyncio + +import httpx +from google import genai +from oci_genai_auth import OciSessionAuth + +async def main() -> None: + http_client = httpx.AsyncClient( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ) + + client = genai.Client( + api_key="not-used", + http_options={ + "base_url": "https://", + "httpx_async_client": http_client, + }, + ) + + response = await client.aio.models.generate_content( + model="gemini-2.0-flash-001", + contents="Write a one-sentence bedtime story about a unicorn.", + ) + print(response) + await http_client.aclose() + +asyncio.run(main()) +``` + +--- + +### Anthropic + +#### Using the Native Anthropic Client + +```python +import httpx +from anthropic import Anthropic +from oci_genai_auth import OciSessionAuth + +client = Anthropic( + api_key="not-used", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", + http_client=httpx.Client( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), +) + +message = client.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=256, + messages=[{"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."}], +) +print(message) +``` + +#### Async Messages + +```python +import asyncio + +import httpx +from anthropic import AsyncAnthropic +from oci_genai_auth import OciSessionAuth + +async def main() -> None: + client = AsyncAnthropic( + api_key="not-used", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", + http_client=httpx.AsyncClient( + auth=OciSessionAuth(profile_name=""), + headers={ + "CompartmentId": "", + "opc-compartment-id": "", + }, + ), + ) + + message = await client.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=256, + messages=[{"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."}], + ) + print(message) + await client.close() + +asyncio.run(main()) +``` + +--- + +### OCI Data Science Model Deployment + +#### Using the Native OpenAI Client + +```python + +import httpx +from openai import OpenAI +from oci_genai_auth import OciSessionAuth + +# Example for OCI Data Science Model Deployment endpoint +client = OpenAI( + api_key="not-used", + base_url="https://modeldeployment.us-ashburn-1.oci.customer-oci.com//predict/v1", + http_client=httpx.Client(auth=OciSessionAuth()), +) + +response = client.chat.completions.create( + model="", + messages=[ + { + "role": "user", + "content": "Explain how to list all files in a directory using Python.", + }, + ], +) +print(response.model_dump_json()) +``` + +### Signers + +The library supports multiple OCI authentication methods (signers). Choose the one that matches your runtime environment and security posture. + +Supported signers + +- `OciSessionAuth` — Uses an OCI session token from your local OCI CLI profile. +- `OciResourcePrincipalAuth` — Uses Resource Principal auth. +- `OciInstancePrincipalAuth` — Uses Instance Principal auth. Best for OCI Compute instances with dynamic group policies. +- `OciUserPrincipalAuth` — Uses an OCI user API key. Suitable for service accounts/automation where API keys are managed securely. + +Minimal examples of constructing each auth type: + +```python +from oci_genai_auth import ( + OciSessionAuth, + OciResourcePrincipalAuth, + OciInstancePrincipalAuth, + OciUserPrincipalAuth, +) + +# 1) Session (local dev; uses ~/.oci/config + session token) +session_auth = OciSessionAuth(profile_name="DEFAULT") + +# 2) Resource Principal (OCI services with RP) +rp_auth = OciResourcePrincipalAuth() + +# 3) Instance Principal (OCI Compute) +ip_auth = OciInstancePrincipalAuth() + +# 4) User Principal (API key in ~/.oci/config) +up_auth = OciUserPrincipalAuth(profile_name="DEFAULT") +``` + +--- + +## Contributing + +This project welcomes contributions from the community. +Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md). + +--- + +## Security + +Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process. + +--- + +## License + +Copyright (c) 2026 Oracle and/or its affiliates. + +Released under the Universal Permissive License v1.0 as shown at +[https://oss.oracle.com/licenses/upl/](https://oss.oracle.com/licenses/upl/) diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt new file mode 100644 index 0000000..239402c --- /dev/null +++ b/THIRD_PARTY_LICENSES.txt @@ -0,0 +1,262 @@ +OCI OpenAI Client for Python Third Party License File + +------------------------ Third Party Components ------------------------ +------------------------------- Licenses ------------------------------- +- Apache License 2.0 +- MIT License +- Python Software Foundation License 2.0 (PSF-2.0) +- Universal Permissive License (UPL) + +======================== Third Party Components ======================== +httpx +* Copyright (c) 2021 ProjectDiscovery, Inc. +* License: MIT License +* Source code: https://github.com/projectdiscovery/httpx +* Project home: https://github.com/projectdiscovery/httpx + +oci +* Copyright (c) 2016, 2020, Oracle and/or its affiliates. [see notices section above] +* License: Apache License 2.0, Universal Permissive License (UPL) +* Source code: https://github.com/oracle/oci-python-sdk +* Project home: https://docs.oracle.com/en-us/iaas/tools/python/2.44.0/index.html + +requests +* Copyright 2023 Kenneth Reitz +* License: Apache-2.0 license +* Source code: https://github.com/psf/requests +* Project home: https://requests.readthedocs.io/en/latest/ + +=============================== Licenses =============================== + +------------------------------ MIT License ----------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------ + +------------------Universal Permissive License (UPL) ------------------- + +Subject to the condition set forth below, permission is hereby granted to any person +obtaining a copy of this software, associated documentation and/or data (collectively +the "Software"), free of charge and under any and all copyright rights in the Software, +and any and all patent rights owned or freely licensable by each licensor hereunder +covering either (i) the unmodified Software as contributed to or provided by such +licensor, or (ii) the Larger Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is +included with the Software (each a "Larger Work" to which the Software is contributed +by such licensors), + +without restriction, including without limitation the rights to copy, create derivative +works of, display, perform, and distribute the Software and make, use, sell, offer for +sale, import, export, have made, and have sold the Software and the Larger Work(s), and +to sublicense the foregoing rights on either these or other terms. + +This license is subject to the following condition: + +The above copyright notice and either this complete permission notice or at a minimum a +reference to the UPL must be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------ + +-------------------------- Apache License 2.0 -------------------------- + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------ diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/anthropic/basic_messages.py b/examples/anthropic/basic_messages.py new file mode 100644 index 0000000..fae2ed9 --- /dev/null +++ b/examples/anthropic/basic_messages.py @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from examples import common + +MODEL = "claude-opus-4-6" + + +def main() -> None: + client = common.build_anthropic_client() + + message = client.messages.create( + model=MODEL, + max_tokens=256, + messages=[ + {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} + ], + ) + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/basic_messages_api_key.py b/examples/anthropic/basic_messages_api_key.py new file mode 100644 index 0000000..29a0bdd --- /dev/null +++ b/examples/anthropic/basic_messages_api_key.py @@ -0,0 +1,27 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + + +from anthropic import Anthropic + +MODEL = "claude-opus-4-6" + + +def main() -> None: + client = Anthropic( + api_key="<>", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", + ) + + message = client.messages.create( + model=MODEL, + max_tokens=256, + messages=[ + {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} + ], + ) + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/basic_messages_async.py b/examples/anthropic/basic_messages_async.py new file mode 100644 index 0000000..95d29bd --- /dev/null +++ b/examples/anthropic/basic_messages_async.py @@ -0,0 +1,26 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import asyncio + +from examples import common + +MODEL = "claude-opus-4-6" + + +async def main() -> None: + client = common.build_anthropic_async_client() + + message = await client.messages.create( + model=MODEL, + max_tokens=256, + messages=[ + {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} + ], + ) + print(message.model_dump_json(indent=2)) + await client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/anthropic/messages_context_management.py b/examples/anthropic/messages_context_management.py new file mode 100644 index 0000000..250d421 --- /dev/null +++ b/examples/anthropic/messages_context_management.py @@ -0,0 +1,56 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + +MODEL = "claude-opus-4-6" +FILE_PATH = "" + + +def _build_client(): + return common.build_anthropic_client() + + +def main() -> None: + if not FILE_PATH: + raise ValueError( + "Set large FILE_PATH to the document you want to summarize (this will test compaction)." + ) + + client = _build_client() + with open(FILE_PATH) as f: + data = f.read() + print(f"Number of tokens {len(data)/5.0}") + + message = client.beta.messages.create( + model=MODEL, + max_tokens=400, + betas=["compact-2026-01-12"], + context_management={ + "edits": [ + { + "type": "compact_20260112", + "trigger": {"type": "input_tokens", "value": 50000}, + } + ] + }, + messages=[ + { + "role": "user", + "content": f"{data}", + }, + { + "role": "user", + "content": "The above is the research paper, give me 10 main learnings " + "from there. Keep it succint.", + }, + ], + ) + + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_image_text.py b/examples/anthropic/messages_image_text.py new file mode 100644 index 0000000..b1824d8 --- /dev/null +++ b/examples/anthropic/messages_image_text.py @@ -0,0 +1,42 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import base64 +from pathlib import Path + +from examples import common + +MODEL = "claude-opus-4-6" +IMAGE_PATH = Path(__file__).resolve().parents[1] / "oci_openai" / "responses" / "Cat.jpg" + + +def main() -> None: + client = common.build_anthropic_client() + + image_data = base64.b64encode(IMAGE_PATH.read_bytes()).decode("utf-8") + + message = client.messages.create( + model=MODEL, + max_tokens=256, + messages=[ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": image_data, + }, + }, + {"type": "text", "text": "What's in this image?"}, + ], + } + ], + ) + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_memory.py b/examples/anthropic/messages_memory.py new file mode 100644 index 0000000..1ca2974 --- /dev/null +++ b/examples/anthropic/messages_memory.py @@ -0,0 +1,175 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from typing import Dict, List + +from examples import common + +MEMORY_ROOT = "/memories" + +MODEL = "claude-opus-4-6" + + +def _build_client(): + return common.build_anthropic_client() + + +def _normalize_path(path: str) -> str | None: + if not path.startswith(MEMORY_ROOT): + return None + parts = [part for part in path.split("/") if part and part != "."] + if ".." in parts: + return None + return "/" + "/".join(parts) + + +def _format_file_listing(memory_files: Dict[str, str]) -> str: + if not memory_files: + return "No stored memories yet." + + lines = ["Stored memories:"] + for filename in sorted(memory_files): + size = len(memory_files[filename].encode("utf-8")) + lines.append(f"- {filename} ({size} bytes)") + return "\n".join(lines) + + +def _format_file_content(file_text: str, view_range: List[int] | None) -> str: + lines = file_text.splitlines() + if view_range and len(view_range) == 2: + start = max(1, view_range[0]) + end = min(len(lines), view_range[1]) + lines = lines[start - 1 : end] + + numbered = [f"{index + 1}: {line}" for index, line in enumerate(lines)] + return "\n".join(numbered) if numbered else "(file is empty)" + + +def _handle_memory_command(memory_files: Dict[str, str], tool_input: dict) -> str: + command = tool_input.get("command") + raw_path = tool_input.get("path", "") + path = _normalize_path(raw_path) + + if not command: + return "Error: missing command in memory tool input." + + if not path: + return f"Error: invalid path '{raw_path}'." + + if command == "view": + if path == MEMORY_ROOT: + return _format_file_listing(memory_files) + if path not in memory_files: + return f"Error: '{path}' not found." + return _format_file_content(memory_files[path], tool_input.get("view_range")) + + if command == "create": + if path in memory_files: + return f"Error: '{path}' already exists." + memory_files[path] = tool_input.get("file_text", "") + return f"Created '{path}'." + + if command == "str_replace": + if path not in memory_files: + return f"Error: '{path}' not found." + old = tool_input.get("old_str", "") + new = tool_input.get("new_str", "") + content = memory_files[path] + count = content.count(old) + if count == 0: + return f"Error: '{old}' not found in '{path}'." + if count > 1: + return f"Error: '{old}' found {count} times in '{path}'." + memory_files[path] = content.replace(old, new, 1) + return f"Updated '{path}'." + + if command == "insert": + if path not in memory_files: + return f"Error: '{path}' not found." + insert_line = tool_input.get("insert_line") + if insert_line is None: + return "Error: insert_line is required for insert." + lines = memory_files[path].splitlines() + index = max(0, min(len(lines), int(insert_line))) + insert_text = tool_input.get("text", "") + lines[index:index] = insert_text.splitlines() or [""] + memory_files[path] = "\n".join(lines) + return f"Inserted text into '{path}'." + + if command == "delete": + if path == MEMORY_ROOT: + memory_files.clear() + return "Deleted all memories." + if path not in memory_files: + return f"Error: '{path}' not found." + memory_files.pop(path) + return f"Deleted '{path}'." + + if command == "rename": + if path not in memory_files: + return f"Error: '{path}' not found." + new_path = _normalize_path(tool_input.get("new_path", "")) + if not new_path: + return "Error: new_path is required for rename." + if new_path in memory_files: + return f"Error: '{new_path}' already exists." + memory_files[new_path] = memory_files.pop(path) + return f"Renamed '{path}' to '{new_path}'." + + return f"Error: unsupported command '{command}'." + + +def main() -> None: + client = _build_client() + memory_files: Dict[str, str] = {} + + messages = [ + { + "role": "user", + "content": "Remember that I prefer concise summaries and my favorite color is green.", + } + ] + + while True: + response = client.beta.messages.create( + model=MODEL, + max_tokens=256, + betas=["context-management-2025-06-27"], + tools=[ + { + "type": "memory_20250818", + "name": "memory", + } + ], + messages=messages, + ) + + print(response.model_dump_json(indent=2)) + + if response.stop_reason != "tool_use": + break + + tool_results = [] + for block in response.content: + if block.type != "tool_use": + continue + if block.name != "memory": + continue + + result = _handle_memory_command(memory_files, block.input) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + } + ) + + messages.append({"role": "assistant", "content": response.content}) + messages.append({"role": "user", "content": tool_results}) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_speed_fast.py b/examples/anthropic/messages_speed_fast.py new file mode 100644 index 0000000..da42b05 --- /dev/null +++ b/examples/anthropic/messages_speed_fast.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + + +def _build_client(): + return common.build_anthropic_client() + + +MODEL = "claude-opus-4-6" + + +def main() -> None: + client = _build_client() + + message = client.beta.messages.create( + model=MODEL, + max_tokens=200, + betas=["fast-mode-2026-02-01"], + speed="fast", + messages=[ + { + "role": "user", + "content": "List five practical tips for better meeting notes.", + } + ], + ) + + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_streaming.py b/examples/anthropic/messages_streaming.py new file mode 100644 index 0000000..ebb657e --- /dev/null +++ b/examples/anthropic/messages_streaming.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from examples import common + +MODEL = "claude-opus-4-6" + + +def _build_client(): + return common.build_anthropic_client() + + +def main() -> None: + client = _build_client() + + with client.messages.stream( + model=MODEL, + max_tokens=256, + messages=[ + { + "role": "user", + "content": "Write a one-sentence bedtime story about a unicorn.", + } + ], + ) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) + + print() + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_tool_search_client_side.py b/examples/anthropic/messages_tool_search_client_side.py new file mode 100644 index 0000000..1cc2794 --- /dev/null +++ b/examples/anthropic/messages_tool_search_client_side.py @@ -0,0 +1,106 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + +MODEL = "claude-opus-4-6" + + +def _build_client(): + return common.build_anthropic_client() + + +def _handle_tool_use(block): + if block.name == "client_tool_search": + query = (block.input or {}).get("query", "").lower() + references = [] + if "weather" in query: + references.append({"type": "tool_reference", "tool_name": "get_weather"}) + if not references: + return { + "type": "tool_result", + "tool_use_id": block.id, + "content": "No matching tools found.", + } + return { + "type": "tool_result", + "tool_use_id": block.id, + "content": references, + } + + if block.name == "get_weather": + location = (block.input or {}).get("location", "unknown location") + return { + "type": "tool_result", + "tool_use_id": block.id, + "content": f"Weather in {location}: 62F and sunny.", + } + + return { + "type": "tool_result", + "tool_use_id": block.id, + "content": f"Unhandled tool '{block.name}'.", + } + + +def main() -> None: + client = _build_client() + + tools = [ + { + "name": "client_tool_search", + "description": "Search for a relevant tool by keyword.", + "input_schema": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + { + "name": "get_weather", + "description": "Get current weather for a city.", + "defer_loading": True, + "input_schema": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + ] + + messages = [ + { + "role": "user", + "content": "What is the weather in Seattle?", + } + ] + + while True: + response = client.messages.create( + model=MODEL, + max_tokens=256, + system="Use client_tool_search to discover tools before calling them.", + messages=messages, + tools=tools, + tool_choice={"type": "auto"}, + ) + + print(response.model_dump_json(indent=2)) + + if response.stop_reason != "tool_use": + break + + tool_results = [] + for block in response.content: + if block.type != "tool_use": + continue + tool_results.append(_handle_tool_use(block)) + + messages.append({"role": "assistant", "content": response.content}) + messages.append({"role": "user", "content": tool_results}) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_web_fetch.py b/examples/anthropic/messages_web_fetch.py new file mode 100644 index 0000000..fcb33df --- /dev/null +++ b/examples/anthropic/messages_web_fetch.py @@ -0,0 +1,42 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + +MODEL = "claude-opus-4-6" + + +def _build_client(): + return common.build_anthropic_client() + + +def main() -> None: + client = _build_client() + + message = client.beta.messages.create( + model=MODEL, + max_tokens=512, + messages=[ + { + "role": "user", + "content": "Summarize the key points from https://www.anthropic.com" + " in 2-3 sentences.", + } + ], + tools=[ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 1, + } + ], + betas=["web-fetch-2025-09-10"], + ) + + print(message) + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/messages_web_search.py b/examples/anthropic/messages_web_search.py new file mode 100644 index 0000000..6db0b6f --- /dev/null +++ b/examples/anthropic/messages_web_search.py @@ -0,0 +1,41 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + +MODEL = "claude-opus-4-6" + + +def _build_client(): + return common.build_anthropic_client() + + +def main() -> None: + client = _build_client() + + message = client.messages.create( + model=MODEL, + max_tokens=512, + messages=[ + { + "role": "user", + "content": "Find one recent headline about quantum computing and " + "summarize it in one sentence.", + } + ], + tools=[ + { + "type": "web_search_20250305", + "name": "web_search", + "max_uses": 2, + } + ], + ) + + print(message.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/common.py b/examples/common.py new file mode 100644 index 0000000..d85420f --- /dev/null +++ b/examples/common.py @@ -0,0 +1,153 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import httpx + +from oci_genai_auth import OciSessionAuth + +if TYPE_CHECKING: + from anthropic import Anthropic, AsyncAnthropic + from google import genai + from openai import AsyncOpenAI, OpenAI + +# Fill in these values for tests +COMPARTMENT_ID = "" +CONVERSATION_STORE_ID = "" +OPENAI_PROJECT = "" +OVERRIDE_URL = "" +PROFILE_NAME = "DEFAULT" +REGION = "us-chicago-1" +GEMINI_API_KEY = "" +GEMINI_BASE_URL = "" + + +def _build_headers(include_conversation_store_id: bool = False) -> dict[str, str]: + headers: dict[str, str] = {} + if COMPARTMENT_ID: + headers["CompartmentId"] = COMPARTMENT_ID + headers["opc-compartment-id"] = COMPARTMENT_ID + if OPENAI_PROJECT: + headers["OpenAI-Project"] = OPENAI_PROJECT + if include_conversation_store_id and CONVERSATION_STORE_ID: + headers["opc-conversation-store-id"] = CONVERSATION_STORE_ID + return headers + + +def _resolve_openai_base_url() -> str: + service_endpoint = OVERRIDE_URL or ( + f"https://inference.generativeai.{REGION}.oci.oraclecloud.com" if REGION else "" + ) + if not service_endpoint: + raise ValueError("REGION or OVERRIDE_URL must be set.") + return f"{service_endpoint.rstrip(' /')}/openai/v1" + + +def _resolve_anthropic_base_url() -> str: + if not REGION: + raise ValueError("REGION or ANTHROPIC_BASE_URL must be set.") + return f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/anthropic" + + +def _resolve_google_base_url() -> str: + if not REGION: + raise ValueError("REGION or GOOGLE_BASE_URL must be set.") + return f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/google" + + +def build_openai_client() -> "OpenAI": + from openai import OpenAI + + client_kwargs = { + "api_key": "not-used", + "base_url": _resolve_openai_base_url(), + "http_client": httpx.Client( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=_build_headers(include_conversation_store_id=True), + ), + } + if OPENAI_PROJECT: + client_kwargs["project"] = OPENAI_PROJECT + return OpenAI(**client_kwargs) + + +def build_openai_async_client() -> "AsyncOpenAI": + from openai import AsyncOpenAI + + client_kwargs = { + "api_key": "not-used", + "base_url": _resolve_openai_base_url(), + "http_client": httpx.AsyncClient( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=_build_headers(include_conversation_store_id=True), + ), + } + if OPENAI_PROJECT: + client_kwargs["project"] = OPENAI_PROJECT + return AsyncOpenAI(**client_kwargs) + + +def build_anthropic_client() -> "Anthropic": + from anthropic import Anthropic + + return Anthropic( + api_key="not-used", + base_url=_resolve_anthropic_base_url(), + http_client=httpx.Client( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=_build_headers(), + ), + ) + + +def build_anthropic_async_client() -> "AsyncAnthropic": + from anthropic import AsyncAnthropic + + return AsyncAnthropic( + api_key="not-used", + base_url=_resolve_anthropic_base_url(), + http_client=httpx.AsyncClient( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=_build_headers(), + ), + ) + + +def build_google_client() -> "genai.Client": + from google import genai + + headers = _build_headers() + http_client = httpx.Client( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=headers, + ) + return genai.Client( + api_key="not-used", + http_options={ + "base_url": _resolve_google_base_url(), + "headers": headers, + "httpx_client": http_client, + }, + ) + + +def build_google_async_client() -> tuple["genai.Client", httpx.AsyncClient]: + from google import genai + + headers = _build_headers() + http_client = httpx.AsyncClient( + auth=OciSessionAuth(profile_name=PROFILE_NAME), + headers=headers, + ) + client = genai.Client( + api_key="not-used", + http_options={ + "base_url": _resolve_google_base_url(), + "headers": headers, + "httpx_async_client": http_client, + }, + ) + return client, http_client diff --git a/examples/fc_tools.py b/examples/fc_tools.py new file mode 100644 index 0000000..ed26a5d --- /dev/null +++ b/examples/fc_tools.py @@ -0,0 +1,119 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from openai.types.responses.function_tool_param import FunctionToolParam + + +def get_stock_price(symbol: str) -> dict: + """Get the current stock price for a given symbol.""" + print(f"Fetching stock price for {symbol}...") + + # Mock stock data + return { + "symbol": symbol.upper(), + "price": 175.42 if symbol.upper() == "AAPL" else 198.76, + "currency": "USD", + "change": 2.34 if symbol.upper() == "AAPL" else -1.23, + "change_percent": 1.35 if symbol.upper() == "AAPL" else -0.62, + "last_updated": "2025-06-26T15:00:00Z", + } + + +def get_current_weather(location: str, unit: str = "fahrenheit") -> dict: + """Get current weather for a given location. + + Args: + location: The city and state, e.g., San Francisco, CA + unit: The unit of temperature (celsius or fahrenheit) + + Returns: + dict: Weather information + """ + # In a real application, you would call a weather API here + # This is a mock implementation for demonstration + print(f"Fetching weather for {location} in {unit}...") + + # Mock weather data + weather_data = { + "location": location, + "temperature": "72", + "unit": "fahrenheit", + "forecast": ["sunny", "windy"], + "humidity": "65%", + "description": "Sunny with a gentle breeze", + } + + return weather_data + + +def recommend_clothing(temperature, unit="fahrenheit"): + """ + Returns clothing recommendations based on input temperature and unit. + + Parameters: + temperature (float or int): The temperature value to base the recommendation on. + unit (str, optional): The unit of the temperature value. + Can be 'fahrenheit' or 'celsius'. Defaults to 'fahrenheit'. + """ + # Convert to Fahrenheit if input is in Celsius + if unit.lower() == "celsius": + temperature = temperature * 9 / 5 + 32 + + if temperature >= 80: + return "It's hot! Wear shorts and a t-shirt." + elif temperature >= 65: + return "It's warm. A short-sleeve shirt and pants are fine." + elif temperature >= 50: + return "It's a bit chilly. Wear a light jacket or sweater." + elif temperature >= 32: + return "It's cold. Wear a coat, sweater, and possibly a hat." + else: + return "It's freezing! Dress warmly in layers, including a winter coat, gloves, and a hat." + + +fc_tools = [ + FunctionToolParam( + name="get_current_weather", + strict=True, + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and country e.g. Bogotá, Colombia", + } + }, + "required": ["location"], + "additionalProperties": False, + }, + type="function", + description="Get current weather for a given location.", + ), + FunctionToolParam( + name="recommend_clothing", + strict=True, + parameters={ + "type": "object", + "properties": { + "temperature": { + "type": "integer", + "description": "The temperature value to base the recommendation on.", + } + }, + "required": ["temperature"], + "additionalProperties": False, + }, + type="function", + description="Returns clothing recommendations based on input temperature and unit.", + ), +] + + +def execute_function_call(function_name, function_args): + # Call the function + if function_name == "get_current_weather": + return get_current_weather(**function_args) + elif function_name == "recommend_clothing": + return recommend_clothing(**function_args) + else: + return None diff --git a/examples/google/generate_content.py b/examples/google/generate_content.py new file mode 100644 index 0000000..2835f8e --- /dev/null +++ b/examples/google/generate_content.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from examples import common + +MODEL = "google.gemini-2.5-flash" + + +def main() -> None: + client = common.build_google_client() + + response = client.models.generate_content( + model=MODEL, + contents="Write a one-sentence bedtime story about a unicorn.", + ) + print(response.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/google/generate_content_api_key.py b/examples/google/generate_content_api_key.py new file mode 100644 index 0000000..f126e17 --- /dev/null +++ b/examples/google/generate_content_api_key.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from google import genai + +MODEL = "google.gemini-2.5-flash" + + +def main() -> None: + client = genai.Client( + api_key="<>", + http_options={ + "base_url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/google" + }, + ) + + response = client.models.generate_content( + model=MODEL, + contents="Write a one-sentence bedtime story about a unicorn.", + ) + print(response.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/google/generate_content_async.py b/examples/google/generate_content_async.py new file mode 100644 index 0000000..9a86c65 --- /dev/null +++ b/examples/google/generate_content_async.py @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import asyncio + +from examples import common + +MODEL = "google.gemini-2.5-flash" + + +async def main() -> None: + client, http_client = common.build_google_async_client() + + response = await client.aio.models.generate_content( + model=MODEL, + contents="Write a one-sentence bedtime story about a unicorn.", + ) + print(response.model_dump_json(indent=2)) + await http_client.aclose() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/google/generate_content_streaming.py b/examples/google/generate_content_streaming.py new file mode 100644 index 0000000..71c039b --- /dev/null +++ b/examples/google/generate_content_streaming.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from examples import common + +MODEL = "gemini-2.0-flash-001" + + +def _build_client(): + return common.build_google_client() + + +def main() -> None: + client = _build_client() + + stream = client.models.generate_content_stream( + model=MODEL, + contents="Stream a short poem about the ocean in 4 lines.", + ) + + for chunk in stream: + print(chunk) + + +if __name__ == "__main__": + main() diff --git a/examples/google/generate_images.py b/examples/google/generate_images.py new file mode 100644 index 0000000..1906af1 --- /dev/null +++ b/examples/google/generate_images.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from examples import common + +MODEL = "gemini-3.1-flash-image-preview" + + +def main() -> None: + client = common.build_google_client() + + response = client.models.generate_content( + model=MODEL, + contents=["A poster of a mythical dragon in a neon city."], + ) + for part in response.parts: + if part.text is not None: + print(part.text) + elif part.inline_data is not None: + image = part.as_image() + image.save("generated_image.png") + + +if __name__ == "__main__": + main() diff --git a/examples/openai/__init__.py b/examples/openai/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/openai/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/openai/basic_agents_example.py b/examples/openai/basic_agents_example.py new file mode 100644 index 0000000..0137024 --- /dev/null +++ b/examples/openai/basic_agents_example.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +# mypy: ignore-errors + +# OpenAI Agents SDK imports +import asyncio + +from agents import Agent, Runner, set_default_openai_client, trace + +from examples import common + +MODEL = "openai.gpt-4o" + + +def get_oci_openai_client(): + return common.build_openai_async_client() + + +# Set the OCI OpenAI Client as the default client to use with OpenAI Agents +set_default_openai_client(get_oci_openai_client()) + + +async def main(): + agent = Agent(name="Assistant", instructions="You are a helpful assistant", model=MODEL) + # https://openai.github.io/openai-agents-python/models/#tracing-client-error-401 + with trace("Trace workflow"): + result = await Runner.run(agent, "Write a haiku about recursion in programming.") + print(result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/openai/chat_completion/basic_chat_completion.py b/examples/openai/chat_completion/basic_chat_completion.py new file mode 100644 index 0000000..ac6b589 --- /dev/null +++ b/examples/openai/chat_completion/basic_chat_completion.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + +completion = openai_client.chat.completions.create( + model=MODEL, + messages=[ + {"role": "system", "content": "You are a concise assistant."}, + {"role": "user", "content": "List three creative uses for a paperclip."}, + ], + max_tokens=128, +) + +print(completion.model_dump_json(indent=2)) diff --git a/examples/openai/chat_completion/basic_chat_completion_api_key.py b/examples/openai/chat_completion/basic_chat_completion_api_key.py new file mode 100644 index 0000000..035326f --- /dev/null +++ b/examples/openai/chat_completion/basic_chat_completion_api_key.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + + +from openai import OpenAI + +MODEL = "openai.gpt-4.1" + + +def main() -> None: + client = OpenAI( + api_key="<>", + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", + ) + + response = client.chat.completions.create( + model=MODEL, + messages=[ + { + "role": "system", + "content": "You are a concise assistant who answers in one paragraph.", + }, + { + "role": "user", + "content": "Explain why the sky is blue as if you were a physics teacher.", + }, + ], + ) + + print(response.choices[0].message.content) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/chat_completion/streaming_chat_completion.py b/examples/openai/chat_completion/streaming_chat_completion.py new file mode 100644 index 0000000..6925671 --- /dev/null +++ b/examples/openai/chat_completion/streaming_chat_completion.py @@ -0,0 +1,30 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + +stream = openai_client.chat.completions.create( + model=MODEL, + messages=[ + { + "role": "system", + "content": "You are a concise assistant who answers in one paragraph.", + }, + { + "role": "user", + "content": "Explain why the sky is blue as if you were a physics teacher.", + }, + ], + stream=True, +) + +for chunk in stream: + for choice in chunk.choices: + delta = choice.delta + if delta.content: + print(delta.content, end="", flush=True) +print() diff --git a/examples/openai/chat_completion/tool_call_chat_completion.py b/examples/openai/chat_completion/tool_call_chat_completion.py new file mode 100644 index 0000000..d801b44 --- /dev/null +++ b/examples/openai/chat_completion/tool_call_chat_completion.py @@ -0,0 +1,87 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import json +from typing import Dict + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + + +def get_current_weather(location: str, unit: str = "fahrenheit") -> Dict[str, str]: + # Simple stand-in for a real weather lookup. + return { + "location": location, + "temperature": "72", + "unit": unit, + "forecast": ["sunny", "windy"], + } + + +messages = [ + { + "role": "user", + "content": "What is the weather like in Boston and San Francisco?", + } +] +tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather for a specific location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and state, for example Boston, MA.", + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "Temperature unit to use in the response.", + }, + }, + "required": ["location"], + }, + }, + } +] + +first_response = openai_client.chat.completions.create( + model=MODEL, + messages=messages, + tools=tools, + tool_choice="auto", +) +first_choice = first_response.choices[0] + +if first_choice.finish_reason == "tool_calls": + call_message = first_choice.message + new_messages = messages + [call_message] + for tool_call in call_message.tool_calls: + args = json.loads(tool_call.function.arguments) + tool_result = get_current_weather( + location=args.get("location", ""), + unit=args.get("unit", "fahrenheit"), + ) + new_messages.append( + { + "role": "tool", + "name": tool_call.function.name, + "tool_call_id": tool_call.id, + "content": json.dumps(tool_result), + } + ) + + follow_up = openai_client.chat.completions.create( + model=MODEL, + messages=new_messages, + ) + print(follow_up.choices[0].message.content) +else: + print(first_choice.message.content) diff --git a/examples/openai/conversation_items/create_items.py b/examples/openai/conversation_items/create_items.py new file mode 100644 index 0000000..c8006c0 --- /dev/null +++ b/examples/openai/conversation_items/create_items.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +items = openai_client.conversations.items.create( + "conv_977e8f9d624849a79b8eca5e6d67f69a", + items=[ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Hello!"}], + }, + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "How are you?"}], + }, + ], +) +print(items.data) diff --git a/examples/openai/conversation_items/delete_item.py b/examples/openai/conversation_items/delete_item.py new file mode 100644 index 0000000..886eb69 --- /dev/null +++ b/examples/openai/conversation_items/delete_item.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +conversation = openai_client.conversations.items.delete( + conversation_id="conv_977e8f9d624849a79b8eca5e6d67f69a", + item_id="msg_f7cfcdcf47c944cebb414a495e3c3721", +) +print(conversation) diff --git a/examples/openai/conversation_items/list_items.py b/examples/openai/conversation_items/list_items.py new file mode 100644 index 0000000..077934c --- /dev/null +++ b/examples/openai/conversation_items/list_items.py @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +items = openai_client.conversations.items.list("conv_977e8f9d624849a79b8eca5e6d67f69a", limit=10) +print(items.data) diff --git a/examples/openai/conversation_items/retrieve_item.py b/examples/openai/conversation_items/retrieve_item.py new file mode 100644 index 0000000..e7169ff --- /dev/null +++ b/examples/openai/conversation_items/retrieve_item.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +item = openai_client.conversations.items.retrieve( + conversation_id="conv_977e8f9d624849a79b8eca5e6d67f69a", + item_id="msg_f7cfcdcf47c944cebb414a495e3c3721", +) +print(item) diff --git a/examples/openai/conversations/create_conversation.py b/examples/openai/conversations/create_conversation.py new file mode 100644 index 0000000..a2ad31b --- /dev/null +++ b/examples/openai/conversations/create_conversation.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +conversation = openai_client.conversations.create( + metadata={"topic": "demo"}, items=[{"type": "message", "role": "user", "content": "Hello!"}] +) +print(conversation) diff --git a/examples/openai/conversations/delete_conversation.py b/examples/openai/conversations/delete_conversation.py new file mode 100644 index 0000000..1d4f60e --- /dev/null +++ b/examples/openai/conversations/delete_conversation.py @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +deleted = openai_client.conversations.delete("conv_b485050b69e54a12ae82cb2688a7217d") +print(deleted) diff --git a/examples/openai/conversations/retrieve_conversation.py b/examples/openai/conversations/retrieve_conversation.py new file mode 100644 index 0000000..06fb772 --- /dev/null +++ b/examples/openai/conversations/retrieve_conversation.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +conversation = openai_client.conversations.retrieve( + "conv_ord_wypqdfsxjfygwh0w4n5w00c3ucomut08y1p5zsogz3o709ug" +) +print(conversation) diff --git a/examples/openai/conversations/update_conversation.py b/examples/openai/conversations/update_conversation.py new file mode 100644 index 0000000..12009c1 --- /dev/null +++ b/examples/openai/conversations/update_conversation.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +updated = openai_client.conversations.update( + "conv_b485050b69e54a12ae82cb2688a7217d", metadata={"topic": "project-x"} +) +print(updated) diff --git a/examples/openai/create_responses_mcp.py b/examples/openai/create_responses_mcp.py new file mode 100644 index 0000000..1d53b3c --- /dev/null +++ b/examples/openai/create_responses_mcp.py @@ -0,0 +1,31 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + + +def main(): + model = "openai.gpt-4.1" + tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "never", + # "authorization": "key", + "server_url": "https://mcp.deepwiki.com/mcp", + } + ] + + # First Request + response1 = openai_client.responses.create( + model=model, input="please tell me structure about facebook/react", tools=tools, store=False + ) + print(response1.output) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/create_responses_multiturn.py b/examples/openai/create_responses_multiturn.py new file mode 100644 index 0000000..c6a94f8 --- /dev/null +++ b/examples/openai/create_responses_multiturn.py @@ -0,0 +1,40 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + + +def main(): + model = "openai.gpt-4.1" + + # First Request + response1 = openai_client.responses.create( + model=model, + input="Explain what OKRs are in 2 sentences.", + previous_response_id=None, # root of the history + ) + print(response1.output) + + # Second Request with previousResponseId of first request + response2 = openai_client.responses.create( + model=model, + input="Based on that, list 3 common pitfalls to avoid.", + previous_response_id=response1.id, # new branch from response1 + ) + print(response2.output) + + # Second Request with previousResponseId of third request + response3 = openai_client.responses.create( + model=model, + input="Expand bit more on OKRs in a paragraph summary.", + previous_response_id=response1.id, # new branch from response1 + ) + print(response3.output) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/create_responses_websearch.py b/examples/openai/create_responses_websearch.py new file mode 100644 index 0000000..5041fc8 --- /dev/null +++ b/examples/openai/create_responses_websearch.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + + +def main(): + model = "openai.gpt-4.1" + + tools = [ + { + "type": "web_search", + } + ] + + # First Request + response1 = openai_client.responses.create( + model=model, input="please tell me today break news", tools=tools, store=False + ) + print(response1.output) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/create_responses_with_conversation_id.py b/examples/openai/create_responses_with_conversation_id.py new file mode 100644 index 0000000..3daa864 --- /dev/null +++ b/examples/openai/create_responses_with_conversation_id.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + + +def main(): + model = "openai.gpt-4.1" + + conversation = openai_client.conversations.create(metadata={"topic": "demo"}) + print(conversation) + + response = openai_client.responses.create( + model=model, input="Explain what OKRs are in 2 sentences.", conversation=conversation.id + ) + print(response.output) + + response = openai_client.responses.create( + model=model, input="what was my previous question from user?", conversation=conversation.id + ) + print(response.output) + + response = openai_client.responses.create( + model=model, + input="Based on that, list 3 common pitfalls to avoid.", + conversation=conversation.id, + ) + print(response.output) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/function/create_response_fc_ parallel_tool.py b/examples/openai/function/create_response_fc_ parallel_tool.py new file mode 100644 index 0000000..2bcf6d6 --- /dev/null +++ b/examples/openai/function/create_response_fc_ parallel_tool.py @@ -0,0 +1,32 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common +from examples.fc_tools import fc_tools + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + +# parrel_call +response = openai_client.responses.create( + model=MODEL, + input="what is the weather in seattle and in new York?", + previous_response_id=None, # root of the history + tools=fc_tools, +) +print(response.output) + + +# no parrel_call + +response = openai_client.responses.create( + model=MODEL, + input="what is the weather in seattle and in new York?", + previous_response_id=None, # root of the history + tools=fc_tools, + parallel_tool_calls=False, +) +print(response.output) diff --git a/examples/openai/function/create_responses_fc.py b/examples/openai/function/create_responses_fc.py new file mode 100644 index 0000000..ad2e258 --- /dev/null +++ b/examples/openai/function/create_responses_fc.py @@ -0,0 +1,77 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import json + +from openai.types.responses import ResponseFunctionToolCall +from openai.types.responses.response_input_param import FunctionCallOutput +from rich import print + +from examples import common +from examples.fc_tools import execute_function_call, fc_tools + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + +# Creates first request +response = openai_client.responses.create( + model=MODEL, + input="what is the weather in seattle?", + previous_response_id=None, # root of the history + tools=fc_tools, +) +print(response.output) + +# Based on output if it is function call, execute the function and provide output back +if isinstance(response.output[0], ResponseFunctionToolCall): + obj = response.output[0] + function_name = obj.name + function_args = json.loads(obj.arguments) + + function_response = execute_function_call(function_name, function_args) + + response = openai_client.responses.create( + model=MODEL, + input=[ + FunctionCallOutput( + type="function_call_output", + call_id=obj.call_id, + output=str(function_response), + ) + ], + previous_response_id=response.id, + tools=fc_tools, + ) + print(response.output) + +# Ask followup question related to previoud context +response = openai_client.responses.create( + model=MODEL, + input="what clothes should i wear in this weather?", + previous_response_id=response.id, + tools=fc_tools, +) +print(response.output) + +# Based on FCTool execute the function tool output +if isinstance(response.output[0], ResponseFunctionToolCall): + obj = response.output[0] + function_name = obj.name + function_args = json.loads(obj.arguments) + + function_response = execute_function_call(function_name, function_args) + + response = openai_client.responses.create( + model=MODEL, + input=[ + FunctionCallOutput( + type="function_call_output", + call_id=obj.call_id, + output=str(function_response), + ) + ], + previous_response_id=response.id, + tools=fc_tools, + ) + print(response.output) diff --git a/examples/openai/mcp/create_response_approval_flow.py b/examples/openai/mcp/create_response_approval_flow.py new file mode 100644 index 0000000..a2928da --- /dev/null +++ b/examples/openai/mcp/create_response_approval_flow.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" +tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "always", + "server_url": "https://mcp.deepwiki.com/mcp", + } +] +response1 = openai_client.responses.create( + model=MODEL, input="please tell me structure about facebook/react", tools=tools, store=True +) + +print(response1.output) + +approve_id = response1.output[1].id +id = response1.id + +approval_response = { + "type": "mcp_approval_response", + "approval_request_id": approve_id, + "approve": True, +} + + +response2 = openai_client.responses.create( + model=MODEL, input=[approval_response], tools=tools, previous_response_id=id +) +print(response2.output) diff --git a/examples/openai/mcp/create_response_two_mcp.py b/examples/openai/mcp/create_response_two_mcp.py new file mode 100644 index 0000000..ff49a22 --- /dev/null +++ b/examples/openai/mcp/create_response_two_mcp.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" +tools = [ + { + "type": "mcp", + "server_label": "stripe", + "require_approval": "never", + "server_url": "https://mcp.stripe.com", + "authorization": "", + }, + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "never", + "server_url": "https://mcp.deepwiki.com/mcp", + }, +] +response1 = openai_client.responses.create( + model=MODEL, + input="Please use stirpe create account with a and a@g.com and " + "use deepwiki understand facebook/react", + tools=tools, + store=True, +) + +print(response1.output) diff --git a/examples/openai/mcp/create_responses_mcp.py b/examples/openai/mcp/create_responses_mcp.py new file mode 100644 index 0000000..a2928da --- /dev/null +++ b/examples/openai/mcp/create_responses_mcp.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" +tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "always", + "server_url": "https://mcp.deepwiki.com/mcp", + } +] +response1 = openai_client.responses.create( + model=MODEL, input="please tell me structure about facebook/react", tools=tools, store=True +) + +print(response1.output) + +approve_id = response1.output[1].id +id = response1.id + +approval_response = { + "type": "mcp_approval_response", + "approval_request_id": approve_id, + "approve": True, +} + + +response2 = openai_client.responses.create( + model=MODEL, input=[approval_response], tools=tools, previous_response_id=id +) +print(response2.output) diff --git a/examples/openai/mcp/create_responses_mcp_auth.py b/examples/openai/mcp/create_responses_mcp_auth.py new file mode 100644 index 0000000..f6335e7 --- /dev/null +++ b/examples/openai/mcp/create_responses_mcp_auth.py @@ -0,0 +1,27 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" +tools = [ + { + "type": "mcp", + "server_label": "stripe", + "require_approval": "never", + "server_url": "https://mcp.stripe.com", + "authorization": "", + } +] +response1 = openai_client.responses.create( + model=MODEL, + input="Please use stirpe create account with a and a@g.com", + tools=tools, + store=True, +) + +print(response1.output) diff --git a/examples/openai/multitools/mcp_function.py b/examples/openai/multitools/mcp_function.py new file mode 100644 index 0000000..1258fa8 --- /dev/null +++ b/examples/openai/multitools/mcp_function.py @@ -0,0 +1,45 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + + +tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "never", + "server_url": "https://mcp.deepwiki.com/mcp", + }, + { + "type": "function", + "name": "get_weather", + "description": "function to get_weather", + "parameters": { + "type": "object", + "properties": { + "repoName": {"type": "string", "description": "city or country (e.g. seattle"} + }, + "required": ["repoName"], + "additionalProperties": False, + }, + }, +] + +messages = [ + { + "role": "user", + "content": "please tell me something about facebook/react and get weather for seattle", + } +] + + +# parrel_call +response = openai_client.responses.create(model=MODEL, input=messages, tools=tools) +print(response.output) diff --git a/examples/openai/multitools/mcp_websearch.py b/examples/openai/multitools/mcp_websearch.py new file mode 100644 index 0000000..9ebfc96 --- /dev/null +++ b/examples/openai/multitools/mcp_websearch.py @@ -0,0 +1,32 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from rich import print + +from examples import common + +openai_client = common.build_openai_client() + +MODEL = "openai.gpt-4.1" + + +tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "never", + "server_url": "https://mcp.deepwiki.com/mcp", + }, + { + "type": "web_search", + }, +] + + +# parrel_call +response = openai_client.responses.create( + model=MODEL, + input="search latest repo related to react, and use deepwiki tell me repo structure", + tools=tools, +) +print(response.output) diff --git a/examples/openai/oci_openai_chat_completions_example.py b/examples/openai/oci_openai_chat_completions_example.py new file mode 100755 index 0000000..bc57e01 --- /dev/null +++ b/examples/openai/oci_openai_chat_completions_example.py @@ -0,0 +1,44 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import logging + +from examples import common + +logging.basicConfig(level=logging.DEBUG) + + +def main(): + client = common.build_openai_client() + model = "meta.llama-4-scout-17b-16e-instruct" + + completion = client.chat.completions.create( + model="openai.gpt-4.1", + messages=[ + { + "role": "user", + "content": "How do I output all files in a directory using Python?", + }, + ], + ) + print(completion.model_dump_json()) + + # Process the stream + print("=" * 80) + print("Process in streaming mode") + streaming = client.chat.completions.create( + model=model, + messages=[ + { + "role": "user", + "content": "How do I output all files in a directory using Python?", + }, + ], + stream=True, + ) + for chunk in streaming: + print(chunk) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/oci_openai_responses_example.py b/examples/openai/oci_openai_responses_example.py new file mode 100644 index 0000000..9edab76 --- /dev/null +++ b/examples/openai/oci_openai_responses_example.py @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +# mypy: ignore-errors +from examples import common + +MODEL = "openai.gpt-4o" + +PROMPT = "Tell me a three sentence bedtime story about a unicorn." + + +def get_oci_openai_client(): + return common.build_openai_client() + + +def main(): + client = get_oci_openai_client() + response = client.responses.create(model=MODEL, input=PROMPT) + print(response.output[0].content[0].text) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/responses/Cat.jpg b/examples/openai/responses/Cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d89427f836a92a185302dbd78d24f240703162a GIT binary patch literal 166820 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 %(*r=*DkrPprWC{Qm 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�?u^3I94N{`4Qbn)Zb3O5hZ}1>Y>S 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.23.0, <1", + "oci>=2.150.1", + "requests>=2.32.1,<3.0.0", +] + +[project.optional-dependencies] +dev = [ + "ruff>=0.5.6,<0.6.0", + "mypy>=1.11.1,<2.0.0", + "black>=24.8.0,<25.0.0", + "isort>=5.13.2,<6.0.0", + "pytest>=8.3.2,<9.0.0", + "pytest-cov>=5.0.0,<6.0.0", + "respx", + "typing-extensions>=4.11, <5", + "openai>=v1.108.1", + "google-genai>=1.0.0", + "anthropic>=0.79.0", + "openai-agents>=0.5.1", + "pytest-mock", + "ag2[openai]", + "pytest-httpx", + "pytest-asyncio", + "eval_type_backport", + "rich>=13.0" +] + +[project.urls] +Documentation = "https://github.com/oracle-samples/oci-genai-auth#readme" +Issues = "https://github.com/oracle-samples/oci-genai-auth/issues" +Source = "https://github.com/oracle-samples/oci-genai-auth" + +[tool.hatch.version] +path = "src/oci_genai_auth/__about__.py" + +[tool.hatch.envs.types] +extra-dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/oci_genai_auth tests}" + +[tool.coverage.run] +source_pkgs = ["oci_genai_auth", "tests"] +branch = true +parallel = true +omit = [ + "src/oci_genai_auth/__about__.py", +] + +[tool.coverage.paths] +oci_genai_auth = ["src/oci_genai_auth", "*/oci-genai-auth/src/oci_genai_auth"] +tests = ["tests", "*/oci-genai-auth/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] + +[tool.ruff] +target-version = "py38" +line-length = 100 +extend-exclude = [ +] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + "G", +] + +[tool.black] +line-length = 100 +target-version = ['py38'] +exclude = '\.venv|build|dist' diff --git a/src/oci_genai_auth/__about__.py b/src/oci_genai_auth/__about__.py new file mode 100644 index 0000000..1a04f1e --- /dev/null +++ b/src/oci_genai_auth/__about__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +__version__ = "1.1.0" diff --git a/src/oci_genai_auth/__init__.py b/src/oci_genai_auth/__init__.py new file mode 100644 index 0000000..1d1716e --- /dev/null +++ b/src/oci_genai_auth/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from .auth import ( + HttpxOciAuth, + OciInstancePrincipalAuth, + OciResourcePrincipalAuth, + OciSessionAuth, + OciUserPrincipalAuth, +) + +__all__ = [ + "HttpxOciAuth", + "OciSessionAuth", + "OciResourcePrincipalAuth", + "OciInstancePrincipalAuth", + "OciUserPrincipalAuth", +] diff --git a/src/oci_genai_auth/auth.py b/src/oci_genai_auth/auth.py new file mode 100644 index 0000000..352071d --- /dev/null +++ b/src/oci_genai_auth/auth.py @@ -0,0 +1,383 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +import logging +import threading +import time +from abc import ABC, abstractmethod +from typing import Any, Generator, Mapping, Optional + +import httpx +import oci +import requests +from oci.config import DEFAULT_LOCATION, DEFAULT_PROFILE +from typing_extensions import TypeAlias, override + +logger = logging.getLogger(__name__) + +OciAuthSigner: TypeAlias = oci.signer.AbstractBaseSigner + + +class HttpxOciAuth(httpx.Auth, ABC): + """ + Enhanced custom HTTPX authentication class that implements OCI request signing + with auto-refresh. + + This class handles the authentication flow for HTTPX requests by signing them + using the OCI Signer, which adds the necessary authentication headers for OCI API calls. + It also provides automatic token refresh functionality for token-based authentication methods. + Attributes: + signer (oci.signer.Signer): The OCI signer instance used for request signing + refresh_interval: Seconds between token refreshes (default: 3600 - 1 hour) + _lock: Threading lock for thread-safe token refresh + _last_refresh: Last refresh timestamp + """ + + def __init__(self, signer: OciAuthSigner, refresh_interval: int = 3600): + """ + Initialize the authentication with a signer and refresh configuration. + Args: + signer: OCI signer instance + refresh_interval: Seconds between token refreshes (default: 3600 - 1 hour) + """ + self.signer = signer + self.refresh_interval = refresh_interval + self._lock = threading.Lock() + self._last_refresh: Optional[float] = time.time() + logger.info( + "Initialized %s with refresh interval: %d seconds", + self.__class__.__name__, + refresh_interval, + ) + + def _should_refresh_token(self) -> bool: + """ + Check if the token should be refreshed based on time interval. + Returns: + bool: True if token should be refreshed, False otherwise + """ + if not self._last_refresh: + return True + current_time = time.time() + return (current_time - self._last_refresh) >= self.refresh_interval + + @abstractmethod + def _refresh_signer(self) -> None: + """ + Abstract method to refresh the signer. Must be implemented by subclasses. + This method should create a new signer instance with fresh credentials/tokens. + """ + pass + + def _refresh_if_needed(self) -> None: + """ + Refresh the signer if enough time has passed since last refresh. + This method is thread-safe and will only refresh once per interval. + """ + with self._lock: + if self._should_refresh_token(): + logger.info("Time interval reached, refreshing %s ...", self.__class__.__name__) + try: + self._refresh_signer() + self._last_refresh = time.time() + logger.info("%s token refresh completed successfully", self.__class__.__name__) + except Exception as e: + logger.exception("Warning: Token refresh failed:", e) + + def _sign_request(self, request: httpx.Request, content: bytes) -> None: + """ + Sign the given HTTPX request with the OCI signer using the provided content. + Updates request.headers in place with the signed headers. + """ + # Strip any SDK auth headers to avoid conflicting with OCI signing. + request.headers.pop("Authorization", None) + request.headers.pop("X-Api-Key", None) + request.headers.pop("x-goog-api-key", None) + + # Remove Google API key query parameter if present. + params = list(request.url.params.multi_items()) + if params: + filtered = [(key, value) for key, value in params if key.lower() != "key"] + if len(filtered) != len(params): + request.url = request.url.copy_with(params=filtered) + req = requests.Request( + method=request.method, + url=str(request.url), + headers=dict(request.headers), + data=content, + ) + prepared_request = req.prepare() + self.signer.do_request_sign(prepared_request) # type: ignore + request.headers.update(prepared_request.headers) + + @override + def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]: + """ + Authentication flow for HTTPX requests with automatic retry on 401 errors. + This method: + 1. Checks if token needs refresh and refreshes if necessary + 2. Signs the request using OCI signer + 3. Yields the signed request + 4. If 401 error is received, attempts token refresh and retries once + Args: + request: The HTTPX request to be authenticated + Yields: + httpx.Request: The authenticated request + """ + # Check and refresh token if needed + self._refresh_if_needed() + + # Read the request content to handle streaming requests properly + try: + content = request.content + except httpx.RequestNotRead: + # For streaming requests, we need to read the content first + content = request.read() + + self._sign_request(request, content) + + response = yield request + + # If we get a 401 (Unauthorized), try refreshing the token once and retry + if response.status_code == 401: + logger.info("Received 401 Unauthorized, attempting token refresh and retry...") + with self._lock: + try: + self._refresh_signer() + self._last_refresh = time.time() + self._sign_request(request, content) + yield request + except Exception as e: + logger.exception("Token refresh on 401 failed:", e) + + +class OciSessionAuth(HttpxOciAuth): + """ + OCI authentication implementation using session-based authentication. + + This class implements OCI authentication using a session token and private key + loaded from the OCI configuration file. It's suitable for interactive user sessions. + + Attributes: + signer (oci.auth.signers.SecurityTokenSigner): OCI signer using session token + """ + + def __init__( + self, + config_file: str = DEFAULT_LOCATION, + profile_name: str = DEFAULT_PROFILE, + refresh_interval: int = 3600, + **kwargs: Mapping[str, Any], + ): + """ + Initialize a Security Token-based OCI signer. + + Parameters + ---------- + config_file : str, optional + Path to the OCI configuration file. Defaults to `~/.oci/config`. + profile_name : str, optional + Profile name inside the OCI configuration file to use. + Defaults to "DEFAULT". + refresh_interval: int, optional + Seconds between token refreshes (default: 3600 - 1 hour) + **kwargs : Mapping[str, Any] + Optional keyword arguments: + - `generic_headers`: Optional[Dict[str, str]] + Headers to be used for generic requests. + Default: `["date", "(request-target)", "host"]` + - `body_headers`: Optional[Dict[str, str]] + Headers to be used for signed request bodies. + Default: `["content-length", "content-type", "x-content-sha256"]` + + Raises + ------ + oci.exceptions.ConfigFileNotFound + If the configuration file cannot be found. + KeyError + If a required key such as `"key_file"` is missing in the config. + Exception + For any other initialization errors. + """ + # Load OCI configuration and token + self.config_file = config_file + self.profile_name = profile_name + config = oci.config.from_file(config_file, profile_name) + token = self._load_token(config) + + # Load the private key from config + key_path = config.get("key_file") + if not key_path: + raise KeyError(f"Missing 'key_file' entry in OCI config profile '{profile_name}'.") + private_key = self._load_private_key(config) + + # Optional signer header customization + generic_headers = kwargs.pop("generic_headers", None) + body_headers = kwargs.pop("body_headers", None) + + additional_kwargs = {} + if generic_headers: + additional_kwargs["generic_headers"] = generic_headers + if body_headers: + additional_kwargs["body_headers"] = body_headers + + self.additional_kwargs = additional_kwargs + signer = oci.auth.signers.SecurityTokenSigner(token, private_key, **self.additional_kwargs) + super().__init__(signer=signer, refresh_interval=refresh_interval) + + def _load_token(self, config: Mapping[str, Any]) -> str: + """ + Load session token from file specified in configuration. + Args: + config: OCI configuration dictionary + Returns: + str: Session token content + """ + token_file = config["security_token_file"] + with open(token_file, "r") as f: + return f.read().strip() + + def _load_private_key(self, config: Any) -> str: + """ + Load private key from file specified in configuration. + Args: + config: OCI configuration dictionary + Returns: + Private key object + """ + return oci.signer.load_private_key_from_file(config["key_file"]) + + def _refresh_signer(self) -> None: + """ + Refresh the session signer by reloading token and private key. + This method creates a new SecurityTokenSigner with fresh credentials + loaded from the configuration files. + """ + # Reload configuration in case it has changed + config = oci.config.from_file(self.config_file, self.profile_name) + token = self._load_token(config) + private_key = self._load_private_key(config) + self.signer = oci.auth.signers.SecurityTokenSigner( + token, private_key, **self.additional_kwargs + ) + + +class OciResourcePrincipalAuth(HttpxOciAuth): + """ + OCI authentication implementation using Resource Principal authentication with auto-refresh. + + This class implements OCI authentication using Resource Principal credentials, + which is suitable for services running within OCI (like Functions, Container Instances) + that need to access other OCI services. The resource principal token is automatically + refreshed at specified intervals. + """ + + def __init__(self, refresh_interval: int = 3600, **kwargs: Any) -> None: + """ + Initialize resource principal authentication. + Args: + refresh_interval: Seconds between token refreshes (default: 3600 - 1 hour) + **kwargs: Additional arguments passed to the resource principal signer + """ + self.kwargs = kwargs + signer = oci.auth.signers.get_resource_principals_signer(**kwargs) + super().__init__(signer=signer, refresh_interval=refresh_interval) + + def _refresh_signer(self) -> None: + """ + Refresh the resource principal signer. + This method creates a new resource principal signer which will + automatically fetch fresh credentials from the OCI metadata service. + """ + self.signer = oci.auth.signers.get_resource_principals_signer(**self.kwargs) + + +class OciInstancePrincipalAuth(HttpxOciAuth): + """ + OCI authentication implementation using Instance Principal authentication with auto-refresh. + + This class implements OCI authentication using Instance Principal credentials, + which is suitable for compute instances that need to access OCI services. + The instance principal token is automatically refreshed at specified intervals. + """ + + def __init__(self, refresh_interval: int = 3600, **kwargs) -> None: # noqa: ANN003 + """ + Initialize instance principal authentication. + Args: + refresh_interval: Seconds between token refreshes (default: 3600 - 1 hour) + **kwargs: Additional arguments passed to InstancePrincipalsSecurityTokenSigner + """ + self.kwargs = kwargs + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner(**kwargs) + super().__init__(signer=signer, refresh_interval=refresh_interval) + + def _refresh_signer(self) -> None: + """ + Refresh the instance principal signer. + This method creates a new InstancePrincipalsSecurityTokenSigner which will + automatically fetch fresh credentials from the OCI metadata service. + """ + self.signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner(**self.kwargs) + + +class OciUserPrincipalAuth(HttpxOciAuth): + """ + OCI authentication implementation using user principal authentication with auto-refresh. + + This class implements OCI authentication using API Key credentials loaded from + the OCI configuration file. It's suitable for programmatic access to OCI services. + Since API key authentication doesn't use tokens that expire, this class doesn't + need frequent refresh but supports configuration reload at specified intervals. + Attributes: + config_file (str): Path to OCI configuration file + profile_name (str): Profile name in the configuration file + config (dict): OCI configuration dictionary + """ + + def __init__( + self, + config_file: str = DEFAULT_LOCATION, + profile_name: str = DEFAULT_PROFILE, + refresh_interval: int = 3600, + ) -> None: + """ + Initialize user principal authentication. + Args: + config_file: Path to OCI config file (default: ~/.oci/config) + profile_name: Profile name to use (default: DEFAULT) + refresh_interval: Seconds between config reloads (default: 3600 - 1 hour) + """ + self.config_file = config_file + self.profile_name = profile_name + self.config = oci.config.from_file(config_file, profile_name) + oci.config.validate_config(self.config) + signer = oci.signer.Signer( + tenancy=self.config["tenancy"], + user=self.config["user"], + fingerprint=self.config["fingerprint"], + private_key_file_location=self.config.get("key_file"), + pass_phrase=oci.config.get_config_value_or_default(self.config, "pass_phrase"), + private_key_content=self.config.get("key_content"), + ) + super().__init__(signer=signer, refresh_interval=refresh_interval) + + def _refresh_signer(self) -> None: + """ + Refresh the user principal signer. + For API key authentication, this recreates the signer with the same credentials. + This is mainly useful if the configuration file has been updated. + """ + # Reload configuration in case it has changed + self.config = oci.config.from_file(self.config_file, self.profile_name) + oci.config.validate_config(self.config) + self.signer = oci.signer.Signer( + tenancy=self.config["tenancy"], + user=self.config["user"], + fingerprint=self.config["fingerprint"], + private_key_file_location=self.config.get("key_file"), + pass_phrase=oci.config.get_config_value_or_default(self.config, "pass_phrase"), + private_key_content=self.config.get("key_content"), + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..905a75c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +import os + +import pytest + + +@pytest.fixture(autouse=True, scope="session") +def _disable_openai_agents_tracing(): + # Prevent OpenAI Agents tracing from emitting external HTTP requests during tests. + os.environ.setdefault("OPENAI_AGENTS_DISABLE_TRACING", "true") + try: + from agents.tracing import set_tracing_disabled + except Exception: + yield + return + set_tracing_disabled(True) + yield diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..ae7d6fc --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,146 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +from __future__ import annotations + +from unittest.mock import patch + +import httpx + +from oci_genai_auth.auth import ( + HttpxOciAuth, + OciInstancePrincipalAuth, + OciResourcePrincipalAuth, + OciSessionAuth, + OciUserPrincipalAuth, +) + + +class _DummySigner: + def __init__(self, token: str) -> None: + self.token = token + + def do_request_sign(self, prepared_request) -> None: # noqa: ANN001 + prepared_request.headers["authorization"] = self.token + + +class _DummyAuth(HttpxOciAuth): + def __init__(self, signer: _DummySigner, refresh_interval: int = 3600) -> None: + self.refresh_calls = 0 + super().__init__(signer=signer, refresh_interval=refresh_interval) + + def _refresh_signer(self) -> None: + self.refresh_calls += 1 + self.signer = _DummySigner(f"signed-{self.refresh_calls}") + + +def test_auth_flow_signs_request(): + auth = _DummyAuth(_DummySigner("signed-0")) + request = httpx.Request( + "GET", + "https://example.com?key=secret&foo=bar", + headers={ + "Authorization": "Bearer test", + "X-Api-Key": "api-key", + "x-goog-api-key": "google-key", + }, + ) + flow = auth.auth_flow(request) + signed_request = next(flow) + assert signed_request.headers["authorization"] == "signed-0" + assert "x-api-key" not in signed_request.headers + assert "x-goog-api-key" not in signed_request.headers + assert "key" not in signed_request.url.params + assert signed_request.url.params.get("foo") == "bar" + + +def test_auth_flow_refreshes_on_401(): + auth = _DummyAuth(_DummySigner("signed-0")) + request = httpx.Request("GET", "https://example.com") + flow = auth.auth_flow(request) + signed_request = next(flow) + response = httpx.Response(401, request=signed_request) + retry_request = flow.send(response) + assert auth.refresh_calls == 1 + assert retry_request.headers["authorization"] == "signed-1" + + +def test_refresh_if_needed_calls_refresh_signer(): + auth = _DummyAuth(_DummySigner("signed-0"), refresh_interval=0) + auth._refresh_if_needed() + assert auth.refresh_calls == 1 + + +def test_session_auth_initializes_signer_from_config(): + config = { + "key_file": "dummy.key", + "security_token_file": "dummy.token", + "tenancy": "dummy_tenancy", + "user": "dummy_user", + "fingerprint": "dummy_fingerprint", + } + with patch("oci.config.from_file", return_value=config), patch( + "oci.signer.load_private_key_from_file", return_value="dummy_private_key" + ), patch("oci.auth.signers.SecurityTokenSigner") as mock_signer, patch( + "builtins.open", create=True + ) as mock_open: + mock_open.return_value.__enter__.return_value.read.return_value = "dummy_token" + auth = OciSessionAuth( + profile_name="DEFAULT", + generic_headers=["date"], + body_headers=["content-length"], + ) + + mock_signer.assert_called_once_with( + "dummy_token", + "dummy_private_key", + generic_headers=["date"], + body_headers=["content-length"], + ) + assert auth.signer == mock_signer.return_value + + +def test_user_principal_auth_uses_signer_from_config(): + config = { + "key_file": "dummy.key", + "tenancy": "dummy_tenancy", + "user": "dummy_user", + "fingerprint": "dummy_fingerprint", + } + with patch("oci.config.from_file", return_value=config), patch( + "oci.config.validate_config", return_value=True + ), patch("oci.signer.Signer") as mock_signer: + auth = OciUserPrincipalAuth(profile_name="DEFAULT") + + mock_signer.assert_called_once() + assert auth.signer == mock_signer.return_value + + +def test_resource_principal_refreshes_signer(): + with patch( + "oci.auth.signers.get_resource_principals_signer", return_value="signer-1" + ) as mock_signer: + auth = OciResourcePrincipalAuth() + assert auth.signer == "signer-1" + mock_signer.assert_called_once() + + mock_signer.reset_mock() + mock_signer.return_value = "signer-2" + auth._refresh_signer() + mock_signer.assert_called_once() + assert auth.signer == "signer-2" + + +def test_instance_principal_refreshes_signer(): + with patch( + "oci.auth.signers.InstancePrincipalsSecurityTokenSigner", return_value="signer-1" + ) as mock_signer: + auth = OciInstancePrincipalAuth() + assert auth.signer == "signer-1" + mock_signer.assert_called_once() + + mock_signer.reset_mock() + mock_signer.return_value = "signer-2" + auth._refresh_signer() + mock_signer.assert_called_once() + assert auth.signer == "signer-2" From e743ee958c9ec4e3e7a533a0a0e0ed811d83b923 Mon Sep 17 00:00:00 2001 From: Varun Shenoy Date: Thu, 12 Mar 2026 16:28:04 -0700 Subject: [PATCH 2/4] Addressing review comments and bug. --- .github/workflows/publish-to-pypi.yml | 4 +- examples/common.py | 55 ++++++++----------- ...py => create_response_fc_parallel_tool.py} | 0 pyproject.toml | 6 +- src/oci_genai_auth/auth.py | 24 ++++---- tests/test_auth.py | 19 +++++++ 6 files changed, 59 insertions(+), 49 deletions(-) rename examples/openai/function/{create_response_fc_ parallel_tool.py => create_response_fc_parallel_tool.py} (100%) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 12c7522..acdf77a 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -32,8 +32,8 @@ jobs: - name: Validate run: | WHEEL=$(ls dist/*.whl | head -n 1) - python -m pip install "${WHEEL}[openai,google,anthropic]" - python -c "from oci_genai_auth.openai import OciOpenAI; from oci_genai_auth.google import OciGoogleGenAI; from oci_genai_auth.anthropic import OciAnthropic; import oci_genai_auth;" + python -m pip install "${WHEEL}" + python -c "from oci_genai_auth import OciSessionAuth; from oci_genai_auth import OciUserPrincipalAuth; import oci_genai_auth;" # - name: Publish to Test PyPI # run: | # python -m pip install twine diff --git a/examples/common.py b/examples/common.py index d85420f..6bd28df 100644 --- a/examples/common.py +++ b/examples/common.py @@ -18,44 +18,33 @@ COMPARTMENT_ID = "" CONVERSATION_STORE_ID = "" OPENAI_PROJECT = "" -OVERRIDE_URL = "" PROFILE_NAME = "DEFAULT" -REGION = "us-chicago-1" GEMINI_API_KEY = "" -GEMINI_BASE_URL = "" + +# OpenAI-compatible base URLs. +OPENAI_BASE_URL_PT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1" +OPENAI_BASE_URL_NP = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1" +# Switch to "NP" for examples that store data on the server. +RESPONSE_API_MODE = "PT" # "PT" (pass-through) or "NP" (non-pass-through) + +# Other provider base URLs. +ANTHROPIC_BASE_URL = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic" +GOOGLE_BASE_URL = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/google" def _build_headers(include_conversation_store_id: bool = False) -> dict[str, str]: - headers: dict[str, str] = {} - if COMPARTMENT_ID: - headers["CompartmentId"] = COMPARTMENT_ID - headers["opc-compartment-id"] = COMPARTMENT_ID - if OPENAI_PROJECT: - headers["OpenAI-Project"] = OPENAI_PROJECT - if include_conversation_store_id and CONVERSATION_STORE_ID: + headers: dict[str, str] = { + "CompartmentId": COMPARTMENT_ID, + "opc-compartment-id": COMPARTMENT_ID, + "OpenAI-Project": OPENAI_PROJECT, + } + if include_conversation_store_id: headers["opc-conversation-store-id"] = CONVERSATION_STORE_ID - return headers + return {key: value for key, value in headers.items() if value} def _resolve_openai_base_url() -> str: - service_endpoint = OVERRIDE_URL or ( - f"https://inference.generativeai.{REGION}.oci.oraclecloud.com" if REGION else "" - ) - if not service_endpoint: - raise ValueError("REGION or OVERRIDE_URL must be set.") - return f"{service_endpoint.rstrip(' /')}/openai/v1" - - -def _resolve_anthropic_base_url() -> str: - if not REGION: - raise ValueError("REGION or ANTHROPIC_BASE_URL must be set.") - return f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/anthropic" - - -def _resolve_google_base_url() -> str: - if not REGION: - raise ValueError("REGION or GOOGLE_BASE_URL must be set.") - return f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/google" + return OPENAI_BASE_URL_NP if RESPONSE_API_MODE == "NP" else OPENAI_BASE_URL_PT def build_openai_client() -> "OpenAI": @@ -95,7 +84,7 @@ def build_anthropic_client() -> "Anthropic": return Anthropic( api_key="not-used", - base_url=_resolve_anthropic_base_url(), + base_url=ANTHROPIC_BASE_URL, http_client=httpx.Client( auth=OciSessionAuth(profile_name=PROFILE_NAME), headers=_build_headers(), @@ -108,7 +97,7 @@ def build_anthropic_async_client() -> "AsyncAnthropic": return AsyncAnthropic( api_key="not-used", - base_url=_resolve_anthropic_base_url(), + base_url=ANTHROPIC_BASE_URL, http_client=httpx.AsyncClient( auth=OciSessionAuth(profile_name=PROFILE_NAME), headers=_build_headers(), @@ -127,7 +116,7 @@ def build_google_client() -> "genai.Client": return genai.Client( api_key="not-used", http_options={ - "base_url": _resolve_google_base_url(), + "base_url": GOOGLE_BASE_URL, "headers": headers, "httpx_client": http_client, }, @@ -145,7 +134,7 @@ def build_google_async_client() -> tuple["genai.Client", httpx.AsyncClient]: client = genai.Client( api_key="not-used", http_options={ - "base_url": _resolve_google_base_url(), + "base_url": GOOGLE_BASE_URL, "headers": headers, "httpx_async_client": http_client, }, diff --git a/examples/openai/function/create_response_fc_ parallel_tool.py b/examples/openai/function/create_response_fc_parallel_tool.py similarity index 100% rename from examples/openai/function/create_response_fc_ parallel_tool.py rename to examples/openai/function/create_response_fc_parallel_tool.py diff --git a/pyproject.toml b/pyproject.toml index d590c2b..db4010b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ "pytest-cov>=5.0.0,<6.0.0", "respx", "typing-extensions>=4.11, <5", - "openai>=v1.108.1", + "openai>=1.108.1", "google-genai>=1.0.0", "anthropic>=0.79.0", "openai-agents>=0.5.1", @@ -94,7 +94,7 @@ exclude_lines = [ ] [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 100 extend-exclude = [ ] @@ -114,5 +114,5 @@ select = [ [tool.black] line-length = 100 -target-version = ['py38'] +target-version = ['py39'] exclude = '\.venv|build|dist' diff --git a/src/oci_genai_auth/auth.py b/src/oci_genai_auth/auth.py index 352071d..7e0dc44 100644 --- a/src/oci_genai_auth/auth.py +++ b/src/oci_genai_auth/auth.py @@ -58,8 +58,6 @@ def _should_refresh_token(self) -> bool: Returns: bool: True if token should be refreshed, False otherwise """ - if not self._last_refresh: - return True current_time = time.time() return (current_time - self._last_refresh) >= self.refresh_interval @@ -71,7 +69,7 @@ def _refresh_signer(self) -> None: """ pass - def _refresh_if_needed(self) -> None: + def _refresh_if_needed(self) -> OciAuthSigner: """ Refresh the signer if enough time has passed since last refresh. This method is thread-safe and will only refresh once per interval. @@ -84,9 +82,12 @@ def _refresh_if_needed(self) -> None: self._last_refresh = time.time() logger.info("%s token refresh completed successfully", self.__class__.__name__) except Exception as e: - logger.exception("Warning: Token refresh failed:", e) + logger.exception("Token refresh failed") + return self.signer - def _sign_request(self, request: httpx.Request, content: bytes) -> None: + def _sign_request( + self, request: httpx.Request, content: bytes, signer: OciAuthSigner + ) -> None: """ Sign the given HTTPX request with the OCI signer using the provided content. Updates request.headers in place with the signed headers. @@ -109,7 +110,7 @@ def _sign_request(self, request: httpx.Request, content: bytes) -> None: data=content, ) prepared_request = req.prepare() - self.signer.do_request_sign(prepared_request) # type: ignore + signer.do_request_sign(prepared_request) # type: ignore request.headers.update(prepared_request.headers) @override @@ -127,7 +128,7 @@ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Re httpx.Request: The authenticated request """ # Check and refresh token if needed - self._refresh_if_needed() + signer = self._refresh_if_needed() # Read the request content to handle streaming requests properly try: @@ -136,7 +137,7 @@ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Re # For streaming requests, we need to read the content first content = request.read() - self._sign_request(request, content) + self._sign_request(request, content, signer) response = yield request @@ -147,10 +148,11 @@ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Re try: self._refresh_signer() self._last_refresh = time.time() - self._sign_request(request, content) + signer = self.signer + self._sign_request(request, content, signer) yield request except Exception as e: - logger.exception("Token refresh on 401 failed:", e) + logger.exception("Token refresh on 401 failed") class OciSessionAuth(HttpxOciAuth): @@ -169,7 +171,7 @@ def __init__( config_file: str = DEFAULT_LOCATION, profile_name: str = DEFAULT_PROFILE, refresh_interval: int = 3600, - **kwargs: Mapping[str, Any], + **kwargs: Any, ): """ Initialize a Security Token-based OCI signer. diff --git a/tests/test_auth.py b/tests/test_auth.py index ae7d6fc..83abb84 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -34,6 +34,11 @@ def _refresh_signer(self) -> None: self.signer = _DummySigner(f"signed-{self.refresh_calls}") +class _BrokenRefreshAuth(HttpxOciAuth): + def _refresh_signer(self) -> None: + raise ConnectionError("metadata service unreachable") + + def test_auth_flow_signs_request(): auth = _DummyAuth(_DummySigner("signed-0")) request = httpx.Request( @@ -71,6 +76,20 @@ def test_refresh_if_needed_calls_refresh_signer(): assert auth.refresh_calls == 1 +def test_refresh_failure_does_not_break_auth_flow(caplog): + auth = _BrokenRefreshAuth(_DummySigner("signed-0"), refresh_interval=0) + request = httpx.Request("GET", "https://example.com") + + with caplog.at_level("ERROR"): + flow = auth.auth_flow(request) + signed_request = next(flow) + + assert signed_request.headers["authorization"] == "signed-0" + assert any( + "Token refresh failed" in record.message for record in caplog.records + ) + + def test_session_auth_initializes_signer_from_config(): config = { "key_file": "dummy.key", From 13f62cf4d53c00d997231646ec58e8331e2ecea1 Mon Sep 17 00:00:00 2001 From: Varun Shenoy Date: Thu, 19 Mar 2026 17:17:39 -0700 Subject: [PATCH 3/4] Adding agenthub examples, removing anthropic and google support. --- .github/workflows/run-all-check.yaml | 2 +- README.md | 459 ++---------------- examples/{openai => agenthub}/__init__.py | 0 .../responses => agenthub/openai}/__init__.py | 0 .../openai/agents}/basic_agents_example.py | 11 +- .../converstions/conversation_items_crud.py | 52 ++ .../openai/converstions/conversations_crud.py | 46 ++ examples/agenthub/openai/files/__init__.py | 2 + examples/agenthub/openai/files/files_crud.py | 37 ++ examples/agenthub/openai/files/sample_doc.pdf | Bin 0 -> 74306 bytes .../create_response_fc_parallel_tool.py | 38 ++ .../openai/function/create_responses_fc.py | 85 ++++ .../mcp/create_response_approval_flow.py | 46 ++ .../openai/mcp/create_response_two_mcp.py | 43 ++ .../openai/mcp/create_responses_mcp.py | 46 ++ .../openai/mcp/create_responses_mcp_auth.py} | 21 +- examples/agenthub/openai/memory/__init__.py | 2 + .../openai/memory/long_term_memory.py | 45 ++ .../memory/long_term_memory_access_policy.py | 51 ++ .../memory/short_term_memory_optimization.py | 52 ++ .../agenthub/openai/multiturn/__init__.py | 2 + .../openai/multiturn/conversations_api.py | 36 ++ .../openai/multiturn/responses_chaining.py | 30 ++ .../quickstart_responses_create_api_key.py | 36 ++ .../quickstart_responses_create_oci_iam.py | 28 ++ .../{ => agenthub}/openai/responses/Cat.jpg | Bin .../agenthub/openai/responses/__init__.py | 2 + .../openai/responses/create_response.py | 20 + .../responses/create_response_store_false.py | 25 + .../openai/responses/delete_response.py | 25 + .../responses/file_input_with_file_id.py | 42 ++ .../responses/file_input_with_file_url.py | 35 ++ .../agenthub/openai/responses/get_response.py | 25 + .../responses/image_input_with_base64.py | 48 ++ .../openai/responses/image_input_with_url.py | 35 ++ .../agenthub/openai/responses/reasoning.py | 34 ++ .../openai/responses/streaming_text_delta.py | 26 + .../openai/responses/structured_output.py | 41 ++ .../openai/responses/use_gpt_model.py | 20 + .../openai/responses/use_gptoss_model_dac.py | 20 + .../responses/use_gptoss_model_ondemand.py | 20 + .../openai/responses/use_grok_model.py | 20 + examples/agenthub/openai/tools/__init__.py | 2 + .../agenthub/openai/tools/code_interpreter.py | 27 ++ examples/agenthub/openai/tools/file_search.py | 28 ++ .../agenthub/openai/tools/function_calling.py | 84 ++++ .../agenthub/openai/tools/image_generation.py | 38 ++ .../agenthub/openai/tools/multiple_tools.py | 47 ++ examples/agenthub/openai/tools/remote_mcp.py | 36 ++ .../openai/tools/remote_mcp_approval_flow.py | 60 +++ examples/agenthub/openai/tools/web_search.py | 21 + .../openai/tools/web_search_streaming.py | 24 + .../agenthub/openai/vector_stores/__init__.py | 2 + .../vector_store_file_batches_crud.py | 46 ++ .../vector_stores/vector_store_files_crud.py | 60 +++ .../vector_stores/vector_stores_crud.py | 51 ++ .../vector_stores/vector_stores_search.py | 81 ++++ examples/anthropic/basic_messages.py | 23 - examples/anthropic/basic_messages_api_key.py | 27 -- examples/anthropic/basic_messages_async.py | 26 - .../anthropic/messages_context_management.py | 56 --- examples/anthropic/messages_image_text.py | 42 -- examples/anthropic/messages_memory.py | 175 ------- examples/anthropic/messages_speed_fast.py | 36 -- examples/anthropic/messages_streaming.py | 33 -- .../messages_tool_search_client_side.py | 106 ---- examples/anthropic/messages_web_fetch.py | 42 -- examples/anthropic/messages_web_search.py | 41 -- examples/common.py | 166 +++---- examples/fc_tools.py | 2 + examples/google/generate_content.py | 20 - examples/google/generate_content_api_key.py | 25 - examples/google/generate_content_async.py | 23 - examples/google/generate_content_streaming.py | 28 -- examples/google/generate_images.py | 25 - .../chat_completion/basic_chat_completion.py | 21 - .../streaming_chat_completion.py | 30 -- .../tool_call_chat_completion.py | 87 ---- .../openai/conversation_items/create_items.py | 25 - .../openai/conversation_items/delete_item.py | 14 - .../openai/conversation_items/list_items.py | 11 - .../conversation_items/retrieve_item.py | 14 - .../conversations/create_conversation.py | 13 - .../conversations/delete_conversation.py | 11 - .../conversations/retrieve_conversation.py | 13 - .../conversations/update_conversation.py | 13 - examples/openai/create_responses_multiturn.py | 40 -- examples/openai/create_responses_websearch.py | 28 -- .../create_responses_with_conversation_id.py | 36 -- .../create_response_fc_parallel_tool.py | 32 -- .../openai/function/create_responses_fc.py | 77 --- .../mcp/create_response_approval_flow.py | 38 -- .../openai/mcp/create_response_two_mcp.py | 34 -- examples/openai/mcp/create_responses_mcp.py | 38 -- .../openai/mcp/create_responses_mcp_auth.py | 27 -- examples/openai/multitools/mcp_function.py | 45 -- examples/openai/multitools/mcp_websearch.py | 32 -- .../openai/oci_openai_responses_example.py | 23 - examples/openai/responses/create_response.py | 18 - .../responses/create_response_streaming.py | 25 - .../create_response_with_image_input.py | 34 -- examples/openai/responses/delete_response.py | 13 - examples/openai/responses/get_response.py | 13 - examples/openai/responses/list_input_items.py | 13 - .../websearch/create_responses_websearch.py | 23 - .../create_responses_websearch_source.py | 27 -- examples/partner/__init__.py | 2 + examples/partner/openai/__init__.py | 2 + .../partner/openai/basic_chat_completion.py | 29 ++ .../openai}/basic_chat_completion_api_key.py | 7 +- .../quickstart_openai_chat_completions.py} | 4 +- .../openai/streaming_chat_completion.py | 38 ++ .../openai/tool_call_chat_completion.py | 94 ++++ pyproject.toml | 2 - src/oci_genai_auth/auth.py | 16 +- tests/conftest.py | 2 +- tests/test_auth.py | 28 +- 117 files changed, 2024 insertions(+), 2179 deletions(-) rename examples/{openai => agenthub}/__init__.py (100%) rename examples/{openai/responses => agenthub/openai}/__init__.py (100%) rename examples/{openai => agenthub/openai/agents}/basic_agents_example.py (81%) create mode 100644 examples/agenthub/openai/converstions/conversation_items_crud.py create mode 100644 examples/agenthub/openai/converstions/conversations_crud.py create mode 100644 examples/agenthub/openai/files/__init__.py create mode 100644 examples/agenthub/openai/files/files_crud.py create mode 100644 examples/agenthub/openai/files/sample_doc.pdf create mode 100644 examples/agenthub/openai/function/create_response_fc_parallel_tool.py create mode 100644 examples/agenthub/openai/function/create_responses_fc.py create mode 100644 examples/agenthub/openai/mcp/create_response_approval_flow.py create mode 100644 examples/agenthub/openai/mcp/create_response_two_mcp.py create mode 100644 examples/agenthub/openai/mcp/create_responses_mcp.py rename examples/{openai/create_responses_mcp.py => agenthub/openai/mcp/create_responses_mcp_auth.py} (53%) create mode 100644 examples/agenthub/openai/memory/__init__.py create mode 100644 examples/agenthub/openai/memory/long_term_memory.py create mode 100644 examples/agenthub/openai/memory/long_term_memory_access_policy.py create mode 100644 examples/agenthub/openai/memory/short_term_memory_optimization.py create mode 100644 examples/agenthub/openai/multiturn/__init__.py create mode 100644 examples/agenthub/openai/multiturn/conversations_api.py create mode 100644 examples/agenthub/openai/multiturn/responses_chaining.py create mode 100644 examples/agenthub/openai/quickstart_responses_create_api_key.py create mode 100644 examples/agenthub/openai/quickstart_responses_create_oci_iam.py rename examples/{ => agenthub}/openai/responses/Cat.jpg (100%) create mode 100644 examples/agenthub/openai/responses/__init__.py create mode 100644 examples/agenthub/openai/responses/create_response.py create mode 100644 examples/agenthub/openai/responses/create_response_store_false.py create mode 100644 examples/agenthub/openai/responses/delete_response.py create mode 100644 examples/agenthub/openai/responses/file_input_with_file_id.py create mode 100644 examples/agenthub/openai/responses/file_input_with_file_url.py create mode 100644 examples/agenthub/openai/responses/get_response.py create mode 100644 examples/agenthub/openai/responses/image_input_with_base64.py create mode 100644 examples/agenthub/openai/responses/image_input_with_url.py create mode 100644 examples/agenthub/openai/responses/reasoning.py create mode 100644 examples/agenthub/openai/responses/streaming_text_delta.py create mode 100644 examples/agenthub/openai/responses/structured_output.py create mode 100644 examples/agenthub/openai/responses/use_gpt_model.py create mode 100644 examples/agenthub/openai/responses/use_gptoss_model_dac.py create mode 100644 examples/agenthub/openai/responses/use_gptoss_model_ondemand.py create mode 100644 examples/agenthub/openai/responses/use_grok_model.py create mode 100644 examples/agenthub/openai/tools/__init__.py create mode 100644 examples/agenthub/openai/tools/code_interpreter.py create mode 100644 examples/agenthub/openai/tools/file_search.py create mode 100644 examples/agenthub/openai/tools/function_calling.py create mode 100644 examples/agenthub/openai/tools/image_generation.py create mode 100644 examples/agenthub/openai/tools/multiple_tools.py create mode 100644 examples/agenthub/openai/tools/remote_mcp.py create mode 100644 examples/agenthub/openai/tools/remote_mcp_approval_flow.py create mode 100644 examples/agenthub/openai/tools/web_search.py create mode 100644 examples/agenthub/openai/tools/web_search_streaming.py create mode 100644 examples/agenthub/openai/vector_stores/__init__.py create mode 100644 examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py create mode 100644 examples/agenthub/openai/vector_stores/vector_store_files_crud.py create mode 100644 examples/agenthub/openai/vector_stores/vector_stores_crud.py create mode 100644 examples/agenthub/openai/vector_stores/vector_stores_search.py delete mode 100644 examples/anthropic/basic_messages.py delete mode 100644 examples/anthropic/basic_messages_api_key.py delete mode 100644 examples/anthropic/basic_messages_async.py delete mode 100644 examples/anthropic/messages_context_management.py delete mode 100644 examples/anthropic/messages_image_text.py delete mode 100644 examples/anthropic/messages_memory.py delete mode 100644 examples/anthropic/messages_speed_fast.py delete mode 100644 examples/anthropic/messages_streaming.py delete mode 100644 examples/anthropic/messages_tool_search_client_side.py delete mode 100644 examples/anthropic/messages_web_fetch.py delete mode 100644 examples/anthropic/messages_web_search.py delete mode 100644 examples/google/generate_content.py delete mode 100644 examples/google/generate_content_api_key.py delete mode 100644 examples/google/generate_content_async.py delete mode 100644 examples/google/generate_content_streaming.py delete mode 100644 examples/google/generate_images.py delete mode 100644 examples/openai/chat_completion/basic_chat_completion.py delete mode 100644 examples/openai/chat_completion/streaming_chat_completion.py delete mode 100644 examples/openai/chat_completion/tool_call_chat_completion.py delete mode 100644 examples/openai/conversation_items/create_items.py delete mode 100644 examples/openai/conversation_items/delete_item.py delete mode 100644 examples/openai/conversation_items/list_items.py delete mode 100644 examples/openai/conversation_items/retrieve_item.py delete mode 100644 examples/openai/conversations/create_conversation.py delete mode 100644 examples/openai/conversations/delete_conversation.py delete mode 100644 examples/openai/conversations/retrieve_conversation.py delete mode 100644 examples/openai/conversations/update_conversation.py delete mode 100644 examples/openai/create_responses_multiturn.py delete mode 100644 examples/openai/create_responses_websearch.py delete mode 100644 examples/openai/create_responses_with_conversation_id.py delete mode 100644 examples/openai/function/create_response_fc_parallel_tool.py delete mode 100644 examples/openai/function/create_responses_fc.py delete mode 100644 examples/openai/mcp/create_response_approval_flow.py delete mode 100644 examples/openai/mcp/create_response_two_mcp.py delete mode 100644 examples/openai/mcp/create_responses_mcp.py delete mode 100644 examples/openai/mcp/create_responses_mcp_auth.py delete mode 100644 examples/openai/multitools/mcp_function.py delete mode 100644 examples/openai/multitools/mcp_websearch.py delete mode 100644 examples/openai/oci_openai_responses_example.py delete mode 100644 examples/openai/responses/create_response.py delete mode 100644 examples/openai/responses/create_response_streaming.py delete mode 100644 examples/openai/responses/create_response_with_image_input.py delete mode 100644 examples/openai/responses/delete_response.py delete mode 100644 examples/openai/responses/get_response.py delete mode 100644 examples/openai/responses/list_input_items.py delete mode 100644 examples/openai/websearch/create_responses_websearch.py delete mode 100644 examples/openai/websearch/create_responses_websearch_source.py create mode 100644 examples/partner/__init__.py create mode 100644 examples/partner/openai/__init__.py create mode 100644 examples/partner/openai/basic_chat_completion.py rename examples/{openai/chat_completion => partner/openai}/basic_chat_completion_api_key.py (84%) rename examples/{openai/oci_openai_chat_completions_example.py => partner/openai/quickstart_openai_chat_completions.py} (90%) create mode 100644 examples/partner/openai/streaming_chat_completion.py create mode 100644 examples/partner/openai/tool_call_chat_completion.py diff --git a/.github/workflows/run-all-check.yaml b/.github/workflows/run-all-check.yaml index 293f7a9..dc12f08 100644 --- a/.github/workflows/run-all-check.yaml +++ b/.github/workflows/run-all-check.yaml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/README.md b/README.md index 02e8a55..fa1cdec 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,24 @@ -*This repository acts as a template for all of Oracle’s GitHub repositories. It contains information about the guidelines for those repositories. All files and sections contained in this template are mandatory, and a GitHub app ensures alignment with these guidelines. To get started with a new repository, replace the italic paragraphs with the respective text for your project.* - -# Project name - -*Describe your project's features, functionality and target audience* - -## Installation - -*Provide detailed step-by-step installation instructions. You can name this section **How to Run** or **Getting Started** instead of **Installation** if that's more acceptable for your project* - -## Documentation - -*Developer-oriented documentation can be published on GitHub, but all product documentation must be published on * - -## Examples - -*Describe any included examples or provide a link to a demo/tutorial* - -## Help - -*Inform users on where to get help or how to receive official support from Oracle (if applicable)* - -## Contributing - -*If your project has specific contribution requirements, update the CONTRIBUTING.md file to ensure those requirements are clearly explained* - -This project welcomes contributions from the community. Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md) - -## Security - -Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process - -## License - -*The correct copyright notice format for both documentation and software is* - "Copyright (c) [year,] year Oracle and/or its affiliates." -*You must include the year the content was first released (on any platform) and the most recent year in which it was revised* - -Copyright (c) 2026 Oracle and/or its affiliates. - -*Replace this statement if your project is not licensed under the UPL* - -Released under the Universal Permissive License v1.0 as shown at -. -======= # oci-genai-auth [![PyPI - Version](https://img.shields.io/pypi/v/oci-genai-auth.svg)](https://pypi.org/project/oci-genai-auth) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/oci-genai-auth.svg)](https://pypi.org/project/oci-genai-auth) -The **OCI GenAI Auth** Python library provides secure and convenient access to the OpenAI-compatible REST API hosted by **OCI Generative AI Service** and **OCI Data Science Model Deployment** Service. - ---- +The **OCI GenAI Auth** Python library provides OCI request-signing helpers for the OpenAI-compatible REST APIs hosted by OCI Generative AI. Partner/Passthrough endpoints do not store conversation history on OCI servers, while AgentHub (non-passthrough) stores data on OCI-managed servers. ## Table of Contents -- [oci-genai-auth](#oci-genai-auth) - - [Table of Contents](#table-of-contents) - - [Before You Start](#before-you-start) - - [Installation](#installation) - - [Examples](#examples) - - [OCI Generative AI](#oci-generative-ai) - - [Using the Native OpenAI Client](#using-the-native-openai-client) - - [Using with Langchain](#using-with-langchain-openai) - - [Google Gen AI (Gemini)](#google-gen-ai-gemini) - - [Using the Native Google Gen AI Client](#using-the-native-google-gen-ai-client) - - [Generate Images](#generate-images) - - [Async Generate Content](#async-generate-content) - - [Anthropic](#anthropic) - - [Using the Native Anthropic Client](#using-the-native-anthropic-client) - - [Async Messages](#async-messages) - - [OCI Data Science Model Deployment](#oci-data-science-model-deployment) - - [Using the Native OpenAI Client](#using-the-native-openai-client-1) - - [Signers](#signers) - - [Contributing](#contributing) - - [Security](#security) - - [License](#license) - ---- +- [Before you start](#before-you-start) +- [Using OCI IAM Auth](#using-oci-iam-auth) +- [Using API Key Auth](#using-api-key-auth) +- [Using AgentHub APIs (non-passthrough)](#using-agenthub-apis-non-passthrough) +- [Using Partner APIs (passthrough)](#using-partner-apis-passthrough) +- [Running the Examples](#running-the-examples) ## Before you start **Important!** -This package provides OCI request-signing helpers for `httpx`. It does not ship OCI-specific SDK clients. Use the native OpenAI SDK with a custom `httpx` client that applies OCI signing. +Note that this package, as well as API keys package described below, only supports OpenAI, xAi Grok and Meta LLama models on OCI Generative AI. Before you start using this package, determine if this is the right option for you. @@ -103,396 +39,79 @@ To authorize any API Key allow any-user to use generative-ai-family in compartment where ALL { request.principal.type='generativeaiapikey' } ``` -- Update the `base_url` in your code: - -```python -from openai import OpenAI -import os - -API_KEY=os.getenv("OPENAI_API_KEY") - -print(API_KEY) - -client = OpenAI( - api_key=API_KEY, - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1" -) - -# Responses API -response = client.responses.create( - model="openai.gpt-oss-120b", - # model="xai.grok-3", - # meta models are not supported with the Responses API - input="Write a one-sentence bedtime story about a unicorn." -) -print(response) - -# Completion API -response = client.chat.completions.create( - # model="openai.gpt-oss-120b", - # model="meta.llama-3.3-70b-instruct", - model="xai.grok-3", - messages=[{ - "role": "user", - "content": "Write a one-sentence bedtime story about a unicorn." - } - ] -) -print(response) -``` +## Using OCI IAM Auth - -API Keys offer a seamless transition from code using the openai SDK, and allow usage in 3rd party code or services that don't offer an override of the http client. - -However, if authentication at the user, compute instance, resource or workload level (OKE pods) is preferred, this package is for you. - -It offers the same compatibility with the `openai` SDK, but requires patching the http client. See the following instruction on how to use it. - -## Installation - -```console -pip install oci-genai-auth -``` - -Install the SDKs you plan to use separately, for example: - -```console -pip install openai -pip install anthropic -pip install google-genai -``` - ---- - -## Examples - -### OCI Generative AI - -Notes: - -- **Cohere models do not support OpenAI-compatible API** -- **OCI Generative AI requires the OpenAI SDK base URL to end with `/openai/v1`** - -#### Using the Native OpenAI Client +Use OCI IAM auth when you want to sign requests with your OCI profile (session/user/resource/instance principal) instead of API keys. ```python - import httpx from openai import OpenAI from oci_genai_auth import OciSessionAuth -# Example for OCI Generative AI endpoint client = OpenAI( - api_key="not-used", base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", - http_client=httpx.Client( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), -) - -completion = client.chat.completions.create( - model="", - messages=[ - { - "role": "user", - "content": "How do I output all files in a directory using Python?", - }, - ], -) -print(completion.model_dump_json()) - -``` - -If you only need the auth helpers (no OCI-specific OpenAI shim client), you can import them from -`oci_genai_auth` which only exposes the signer classes used by `httpx`. - -#### Using with langchain-openai - -```python -from langchain_openai import ChatOpenAI -import httpx -from oci_genai_auth import OciUserPrincipalAuth - - -llm = ChatOpenAI( - model="", # for example "xai.grok-4-fast-reasoning" api_key="not-used", - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", - http_client=httpx.Client( - auth=OciUserPrincipalAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), - # use_responses_api=True - # stream_usage=True, - # temperature=None, - # max_tokens=None, - # timeout=None, - # reasoning_effort="low", - # max_retries=2, - # other params... + http_client=httpx.Client(auth=OciSessionAuth(profile_name="DEFAULT")), ) - -messages = [ - ( - "system", - "You are a helpful assistant that translates English to French. Translate the user sentence.", - ), - ("human", "I love programming."), -] -ai_msg = llm.invoke(messages) -print(ai_msg) ``` ---- - -### Google Gen AI (Gemini) - -#### Using the Native Google Gen AI Client +## Using API Key Auth -```python -import httpx -from google import genai -from oci_genai_auth import OciSessionAuth - -client = genai.Client( - api_key="not-used", - http_options={ - "base_url": "https://", - "httpx_client": httpx.Client( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), - }, -) - -response = client.models.generate_content( - model="gemini-2.0-flash-001", - contents="Write a one-sentence bedtime story about a unicorn.", -) -print(response) -``` - -#### Generate Images +Use OCI Generative AI API Keys if you want a direct API-key workflow with the OpenAI SDK. ```python -import httpx -from google import genai -from oci_genai_auth import OciSessionAuth - -client = genai.Client( - api_key="not-used", - http_options={ - "base_url": "https://", - "httpx_client": httpx.Client( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), - }, -) +import os +from openai import OpenAI -response = client.models.generate_images( - model="imagen-3.0-generate-002", - prompt="A poster of a mythical dragon in a neon city.", +client = OpenAI( + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", + api_key=os.getenv("OPENAI_API_KEY"), ) -print(response) ``` -#### Async Generate Content +## Using AgentHub APIs (non-passthrough) -```python -import asyncio - -import httpx -from google import genai -from oci_genai_auth import OciSessionAuth - -async def main() -> None: - http_client = httpx.AsyncClient( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ) - - client = genai.Client( - api_key="not-used", - http_options={ - "base_url": "https://", - "httpx_async_client": http_client, - }, - ) - - response = await client.aio.models.generate_content( - model="gemini-2.0-flash-001", - contents="Write a one-sentence bedtime story about a unicorn.", - ) - print(response) - await http_client.aclose() - -asyncio.run(main()) -``` - ---- - -### Anthropic - -#### Using the Native Anthropic Client +AgentHub runs in non-pass-through mode and provides a unified interface for interacting with models and agentic capabilities. +It is compatible with OpenAI's Responses API and the Open Responses Spec, enabling developers/users to: build agents with OpenAI SDK. +Only the project OCID is required. ```python import httpx -from anthropic import Anthropic +from openai import OpenAI from oci_genai_auth import OciSessionAuth -client = Anthropic( +client = OpenAI( + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", api_key="not-used", - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", - http_client=httpx.Client( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), + project="ocid1.generativeaiproject.oc1.us-chicago-1.aaaaaaaaexample", + http_client=httpx.Client(auth=OciSessionAuth(profile_name="DEFAULT")), ) - -message = client.messages.create( - model="claude-3-5-sonnet-20241022", - max_tokens=256, - messages=[{"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."}], -) -print(message) -``` - -#### Async Messages - -```python -import asyncio - -import httpx -from anthropic import AsyncAnthropic -from oci_genai_auth import OciSessionAuth - -async def main() -> None: - client = AsyncAnthropic( - api_key="not-used", - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", - http_client=httpx.AsyncClient( - auth=OciSessionAuth(profile_name=""), - headers={ - "CompartmentId": "", - "opc-compartment-id": "", - }, - ), - ) - - message = await client.messages.create( - model="claude-3-5-sonnet-20241022", - max_tokens=256, - messages=[{"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."}], - ) - print(message) - await client.close() - -asyncio.run(main()) ``` ---- +## Using Partner APIs (passthrough) -### OCI Data Science Model Deployment - -#### Using the Native OpenAI Client +Partner endpoints run in pass-through mode and require the compartment OCID header. ```python - import httpx from openai import OpenAI from oci_genai_auth import OciSessionAuth -# Example for OCI Data Science Model Deployment endpoint client = OpenAI( + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1", api_key="not-used", - base_url="https://modeldeployment.us-ashburn-1.oci.customer-oci.com//predict/v1", - http_client=httpx.Client(auth=OciSessionAuth()), -) - -response = client.chat.completions.create( - model="", - messages=[ - { - "role": "user", - "content": "Explain how to list all files in a directory using Python.", - }, - ], -) -print(response.model_dump_json()) -``` - -### Signers - -The library supports multiple OCI authentication methods (signers). Choose the one that matches your runtime environment and security posture. - -Supported signers - -- `OciSessionAuth` — Uses an OCI session token from your local OCI CLI profile. -- `OciResourcePrincipalAuth` — Uses Resource Principal auth. -- `OciInstancePrincipalAuth` — Uses Instance Principal auth. Best for OCI Compute instances with dynamic group policies. -- `OciUserPrincipalAuth` — Uses an OCI user API key. Suitable for service accounts/automation where API keys are managed securely. - -Minimal examples of constructing each auth type: - -```python -from oci_genai_auth import ( - OciSessionAuth, - OciResourcePrincipalAuth, - OciInstancePrincipalAuth, - OciUserPrincipalAuth, + default_headers={ + "opc-compartment-id": "ocid1.compartment.oc1..aaaaaaaaexample", + }, + http_client=httpx.Client(auth=OciSessionAuth(profile_name="DEFAULT")), ) - -# 1) Session (local dev; uses ~/.oci/config + session token) -session_auth = OciSessionAuth(profile_name="DEFAULT") - -# 2) Resource Principal (OCI services with RP) -rp_auth = OciResourcePrincipalAuth() - -# 3) Instance Principal (OCI Compute) -ip_auth = OciInstancePrincipalAuth() - -# 4) User Principal (API key in ~/.oci/config) -up_auth = OciUserPrincipalAuth(profile_name="DEFAULT") ``` ---- - -## Contributing - -This project welcomes contributions from the community. -Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md). - ---- - -## Security - -Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process. - ---- -## License +## Running the Examples -Copyright (c) 2026 Oracle and/or its affiliates. +1. Update `examples/common.py` with your `COMPARTMENT_ID`, `PROJECT_OCID`, and set the correct `REGION`. +2. Set the `OPENAI_API_KEY` environment variable when an example uses API key authentication. +3. Install optional dev dependencies: `pip install -e '.[dev]'`. -Released under the Universal Permissive License v1.0 as shown at -[https://oss.oracle.com/licenses/upl/](https://oss.oracle.com/licenses/upl/) +Run an example either by calling its `main()` method or from the command line. diff --git a/examples/openai/__init__.py b/examples/agenthub/__init__.py similarity index 100% rename from examples/openai/__init__.py rename to examples/agenthub/__init__.py diff --git a/examples/openai/responses/__init__.py b/examples/agenthub/openai/__init__.py similarity index 100% rename from examples/openai/responses/__init__.py rename to examples/agenthub/openai/__init__.py diff --git a/examples/openai/basic_agents_example.py b/examples/agenthub/openai/agents/basic_agents_example.py similarity index 81% rename from examples/openai/basic_agents_example.py rename to examples/agenthub/openai/agents/basic_agents_example.py index 0137024..ac611a4 100644 --- a/examples/openai/basic_agents_example.py +++ b/examples/agenthub/openai/agents/basic_agents_example.py @@ -1,9 +1,9 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ -# mypy: ignore-errors -# OpenAI Agents SDK imports +"""Demonstrates running an OpenAI Agents workflow against the AgentHub endpoint.""" + import asyncio from agents import Agent, Runner, set_default_openai_client, trace @@ -12,13 +12,8 @@ MODEL = "openai.gpt-4o" - -def get_oci_openai_client(): - return common.build_openai_async_client() - - # Set the OCI OpenAI Client as the default client to use with OpenAI Agents -set_default_openai_client(get_oci_openai_client()) +set_default_openai_client(common.build_openai_agenthub_async_client()) async def main(): diff --git a/examples/agenthub/openai/converstions/conversation_items_crud.py b/examples/agenthub/openai/converstions/conversation_items_crud.py new file mode 100644 index 0000000..cbe405d --- /dev/null +++ b/examples/agenthub/openai/converstions/conversation_items_crud.py @@ -0,0 +1,52 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates CRUD operations for conversation items in AgentHub.""" + +from examples import common + + +def main(): + # Create an empty conversation + cp_client = common.build_openai_agenthub_client() + conversation = cp_client.conversations.create() + print("\nCreated conversation:", conversation) + + # Create items in the conversation + cp_client.conversations.items.create( + conversation_id=conversation.id, + items=[ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "What's your name?"}], + }, + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "What's your favorite color?"}], + }, + ], + ) + + # List the items in the conversation after creating items + items = cp_client.conversations.items.list( + conversation_id=conversation.id, + ) + print("\nConversation items after creating items:", items.data) + + # Delete an item from the conversation + cp_client.conversations.items.delete( + conversation_id=conversation.id, + item_id=items.data[0].id, + ) + + # List the items in the conversation after deleting an item + items = cp_client.conversations.items.list( + conversation_id=conversation.id, + ) + print("\nConversation items after creating items:", items.data) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/converstions/conversations_crud.py b/examples/agenthub/openai/converstions/conversations_crud.py new file mode 100644 index 0000000..bde95a7 --- /dev/null +++ b/examples/agenthub/openai/converstions/conversations_crud.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates CRUD operations for conversations in AgentHub.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + # Create a conversation + conversation = client.conversations.create( + items=[ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Hello!"}], + } + ], + metadata={"topic": "demo"}, + ) + print("\nCreated conversation:", conversation) + + # Retrieve the conversation + conversation = client.conversations.retrieve( + conversation_id=conversation.id, + ) + print("\nRetrieved conversation:", conversation) + + # Update the conversation with new metadata + conversation = client.conversations.update( + conversation_id=conversation.id, + metadata={"topic": "demo2"}, + ) + print("\nUpdated conversation:", conversation) + + # Delete the conversation + client.conversations.delete( + conversation_id=conversation.id, + ) + print("\nDeleted conversation:", conversation) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/files/__init__.py b/examples/agenthub/openai/files/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/files/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/files/files_crud.py b/examples/agenthub/openai/files/files_crud.py new file mode 100644 index 0000000..ce1710f --- /dev/null +++ b/examples/agenthub/openai/files/files_crud.py @@ -0,0 +1,37 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates CRUD operations for files.""" + +from pathlib import Path + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + # List files in the project + files_list = client.files.list(order="asc") + for file in files_list.data: + print(f"ID: {file.id:<45} Status:{file.status:<10} Name:{file.filename}") + + pdf_file_path = Path(__file__).parent / "sample_doc.pdf" + + # Upload a file + with open(pdf_file_path, "rb") as f: + file = client.files.create(file=f, purpose="user_data") + print("Uploaded file:", file) + + # Retrieve file metadata + retrieved_result = client.files.retrieve(file_id=file.id) + print("\nRetrieved file:", retrieved_result) + print("\nWaiting for file to get processed") + client.files.wait_for_processing(file.id) + + # Delete file + delete_result = client.files.delete(file_id=file.id) + print("\nDelete result:", delete_result) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/files/sample_doc.pdf b/examples/agenthub/openai/files/sample_doc.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7d0fb33dab72c134e459ccb8f8a3b8b2d1c133cd GIT binary patch literal 74306 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%0Du1sWu&QC`XI4+p2_wz0 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", + }, + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "never", + "server_url": "https://mcp.deepwiki.com/mcp", + }, + ] + response1 = openai_client.responses.create( + model=MODEL, + input="Please use stirpe create account with a and a@g.com and " + "use deepwiki understand facebook/react", + tools=tools, + store=True, + ) + + print(response1.output) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/mcp/create_responses_mcp.py b/examples/agenthub/openai/mcp/create_responses_mcp.py new file mode 100644 index 0000000..ad205ba --- /dev/null +++ b/examples/agenthub/openai/mcp/create_responses_mcp.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates creating responses with MCP tools.""" + +from rich import print + +from examples import common + +MODEL = "openai.gpt-4.1" + + +def main(): + openai_client = common.build_openai_agenthub_client() + + tools = [ + { + "type": "mcp", + "server_label": "deepwiki", + "require_approval": "always", + "server_url": "https://mcp.deepwiki.com/mcp", + } + ] + response1 = openai_client.responses.create( + model=MODEL, input="please tell me structure about facebook/react", tools=tools, store=True + ) + + print(response1.output) + + approve_id = response1.output[1].id + id = response1.id + + approval_response = { + "type": "mcp_approval_response", + "approval_request_id": approve_id, + "approve": True, + } + + response2 = openai_client.responses.create( + model=MODEL, input=[approval_response], tools=tools, previous_response_id=id + ) + print(response2.output) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/create_responses_mcp.py b/examples/agenthub/openai/mcp/create_responses_mcp_auth.py similarity index 53% rename from examples/openai/create_responses_mcp.py rename to examples/agenthub/openai/mcp/create_responses_mcp_auth.py index 1d53b3c..854394e 100644 --- a/examples/openai/create_responses_mcp.py +++ b/examples/agenthub/openai/mcp/create_responses_mcp_auth.py @@ -1,29 +1,34 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +"""Demonstrates creating responses with MCP auth.""" + from rich import print from examples import common -openai_client = common.build_openai_client() +MODEL = "openai.gpt-4.1" def main(): - model = "openai.gpt-4.1" + openai_client = common.build_openai_agenthub_client() + tools = [ { "type": "mcp", - "server_label": "deepwiki", + "server_label": "stripe", "require_approval": "never", - # "authorization": "key", - "server_url": "https://mcp.deepwiki.com/mcp", + "server_url": "https://mcp.stripe.com", + "authorization": "", } ] - - # First Request response1 = openai_client.responses.create( - model=model, input="please tell me structure about facebook/react", tools=tools, store=False + model=MODEL, + input="Please use stirpe create account with a and a@g.com", + tools=tools, + store=True, ) + print(response1.output) diff --git a/examples/agenthub/openai/memory/__init__.py b/examples/agenthub/openai/memory/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/memory/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/memory/long_term_memory.py b/examples/agenthub/openai/memory/long_term_memory.py new file mode 100644 index 0000000..0bc99f9 --- /dev/null +++ b/examples/agenthub/openai/memory/long_term_memory.py @@ -0,0 +1,45 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates long-term memory usage in AgentHub.""" + +import time + +from examples import common + +MODEL = "xai.grok-4-1-fast-reasoning" + + +def main(): + client = common.build_openai_agenthub_client() + # First conversation - store preferences + conversation1 = client.conversations.create( + metadata={"memory_subject_id": "user_123456"}, + ) + + response = client.responses.create( + model=MODEL, + input="I like Fish. I don't like Shrimp.", + conversation=conversation1.id, + ) + print("Response 1:", response.output_text) + + # Delay for long-term memory processing + print("Waiting for long-term memory processing...") + time.sleep(10) + + # Second conversation - recall preferences + conversation2 = client.conversations.create( + metadata={"memory_subject_id": "user_123456"}, + ) + + response = client.responses.create( + model=MODEL, + input="What do I like?", + conversation=conversation2.id, + ) + print("Response 2:", response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/memory/long_term_memory_access_policy.py b/examples/agenthub/openai/memory/long_term_memory_access_policy.py new file mode 100644 index 0000000..958e985 --- /dev/null +++ b/examples/agenthub/openai/memory/long_term_memory_access_policy.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates long-term memory access policies in AgentHub.""" + +import time + +from examples import common + +MODEL = "openai.gpt-5.1" + + +def main(): + client = common.build_openai_agenthub_client() + # First conversation - store only (no recall) + conversation1 = client.conversations.create( + metadata={ + "memory_subject_id": "user_123456", + "memory_access_policy": "store_only", + }, + ) + + response = client.responses.create( + model=MODEL, + input="I like Fish. I don't like Shrimp.", + conversation=conversation1.id, + ) + print("Response 1:", response.output_text) + + # Delay for long-term memory processing + print("Waiting for long-term memory processing...") + time.sleep(20) + + # Second conversation - recall only (no new storage) + conversation2 = client.conversations.create( + metadata={ + "memory_subject_id": "user_123456", + "memory_access_policy": "recall_only", + }, + ) + + response = client.responses.create( + model=MODEL, + input="What food do I like?", + conversation=conversation2.id, + ) + print("Response 2:", response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/memory/short_term_memory_optimization.py b/examples/agenthub/openai/memory/short_term_memory_optimization.py new file mode 100644 index 0000000..d1a3bde --- /dev/null +++ b/examples/agenthub/openai/memory/short_term_memory_optimization.py @@ -0,0 +1,52 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates short-term memory optimization in AgentHub.""" + +from examples import common + +MODEL = "xai.grok-4-1-fast-reasoning" + + +def main(): + client = common.build_openai_agenthub_client() + # Create a conversation with STMO enabled + conversation = client.conversations.create( + metadata={"topic": "demo", "short_term_memory_optimization": "True"}, + items=[{"type": "message", "role": "user", "content": "Hello!"}], + ) + + # Multiple turns - STMO will auto-condense the history + response = client.responses.create( + model=MODEL, + input="I like Fish.", + conversation=conversation.id, + ) + print("Turn 1:", response.output_text) + + response = client.responses.create( + model=MODEL, + input="I like Beef.", + conversation=conversation.id, + ) + print("Turn 2:", response.output_text) + + response = client.responses.create( + model=MODEL, + input="I like ice-cream.", + conversation=conversation.id, + ) + print("Turn 3:", response.output_text) + + response = client.responses.create( + model=MODEL, + input="I like coffee.", + conversation=conversation.id, + ) + print("Turn 4:", response.output_text) + + # The STMO summary will be generated automatically + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/multiturn/__init__.py b/examples/agenthub/openai/multiturn/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/multiturn/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/multiturn/conversations_api.py b/examples/agenthub/openai/multiturn/conversations_api.py new file mode 100644 index 0000000..5a8633f --- /dev/null +++ b/examples/agenthub/openai/multiturn/conversations_api.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates a multi-turn flow using the Conversations API.""" + +from examples import common + +MODEL = "xai.grok-4-1-fast-reasoning" + + +def main(): + client = common.build_openai_agenthub_client() + + # Create a conversation upfront + conversation = client.conversations.create(metadata={"topic": "demo"}) + print("Conversation ID:", conversation.id) + + # First turn + response1 = client.responses.create( + model=MODEL, + input="Tell me a joke. Keep it short.", + conversation=conversation.id, + ) + print("Response 1:", response1.output_text) + + # Second turn on the same conversation + response2 = client.responses.create( + model=MODEL, + input="Why is it funny?", + conversation=conversation.id, + ) + print("Response 2:", response2.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/multiturn/responses_chaining.py b/examples/agenthub/openai/multiturn/responses_chaining.py new file mode 100644 index 0000000..ce568ee --- /dev/null +++ b/examples/agenthub/openai/multiturn/responses_chaining.py @@ -0,0 +1,30 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates chaining responses across multiple turns.""" + +from examples import common + +model = "xai.grok-4-1-fast-reasoning" + + +def main(): + client = common.build_openai_agenthub_client() + # First turn + response1 = client.responses.create( + model=model, + input="Tell me a joke. Keep it short.", + ) + print("Response 1:", response1.output_text) + + # Second turn, chaining to the first + response2 = client.responses.create( + model=model, + input="Why is it funny?", + previous_response_id=response1.id, + ) + print("Response 2:", response2.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/quickstart_responses_create_api_key.py b/examples/agenthub/openai/quickstart_responses_create_api_key.py new file mode 100644 index 0000000..75cc908 --- /dev/null +++ b/examples/agenthub/openai/quickstart_responses_create_api_key.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Quickstart using Generative AI API Key authentication. + +This example uses the native OpenAI client with OCI Generative AI API Key. +No oci-genai-auth package needed for API Key auth - just the official OpenAI SDK. + +Steps: + 1. Create a Generative AI Project on OCI Console + 2. Create a Generative AI API Key on OCI Console + 3. Run this script +""" + +import os + +from openai import OpenAI + +from examples.common import PROJECT_OCID + + +def main(): + client = OpenAI( + api_key=os.getenv("OPENAI_API_KEY"), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", + ) + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/quickstart_responses_create_oci_iam.py b/examples/agenthub/openai/quickstart_responses_create_oci_iam.py new file mode 100644 index 0000000..02b3ae6 --- /dev/null +++ b/examples/agenthub/openai/quickstart_responses_create_oci_iam.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Quickstart using OCI IAM authentication. + +This example uses oci-genai-auth with the OpenAI SDK for AgentHub. + +Steps: + 1. Create a Generative AI Project on OCI Console + 2. pip install oci-genai-auth + 3. Run this script +""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/responses/Cat.jpg b/examples/agenthub/openai/responses/Cat.jpg similarity index 100% rename from examples/openai/responses/Cat.jpg rename to examples/agenthub/openai/responses/Cat.jpg diff --git a/examples/agenthub/openai/responses/__init__.py b/examples/agenthub/openai/responses/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/responses/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/responses/create_response.py b/examples/agenthub/openai/responses/create_response.py new file mode 100644 index 0000000..c6ec7ab --- /dev/null +++ b/examples/agenthub/openai/responses/create_response.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates creating a response with the Responses API on AgentHub.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/create_response_store_false.py b/examples/agenthub/openai/responses/create_response_store_false.py new file mode 100644 index 0000000..50e6f96 --- /dev/null +++ b/examples/agenthub/openai/responses/create_response_store_false.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates creating a response with storage disabled using the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + store=False, + ) + print(response.output_text) + + # Try to retrieve the response by ID, and it should throw openai.NotFoundError + retrieved = client.responses.retrieve(response_id=response.id) + print(f"Response: {retrieved}") + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/delete_response.py b/examples/agenthub/openai/responses/delete_response.py new file mode 100644 index 0000000..a41a607 --- /dev/null +++ b/examples/agenthub/openai/responses/delete_response.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates deleting a response from the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + # Create a response first + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print("Created response ID:", response.id) + + # Delete the response by ID + client.responses.delete(response_id=response.id) + print("Deleted response:", response.id) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/file_input_with_file_id.py b/examples/agenthub/openai/responses/file_input_with_file_id.py new file mode 100644 index 0000000..3e8ae9e --- /dev/null +++ b/examples/agenthub/openai/responses/file_input_with_file_id.py @@ -0,0 +1,42 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates providing file input by file ID to the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + # Upload a file first + with open("../files/sample_doc.pdf", "rb") as f: + file = client.files.create( + file=f, + purpose="user_data", + ) + + # Use the file in a response + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input=[ + { + "role": "user", + "content": [ + { + "type": "input_file", + "file_id": file.id, + }, + { + "type": "input_text", + "text": "What's discussed in the file?", + }, + ], + } + ], + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/file_input_with_file_url.py b/examples/agenthub/openai/responses/file_input_with_file_url.py new file mode 100644 index 0000000..2df8274 --- /dev/null +++ b/examples/agenthub/openai/responses/file_input_with_file_url.py @@ -0,0 +1,35 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates providing file input by URL to the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + store=False, + input=[ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Analyze the letter and provide a summary of the key points.", + }, + { + "type": "input_file", + "file_url": "https://www.berkshirehathaway.com/letters/2024ltr.pdf", + }, + ], + } + ], + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/get_response.py b/examples/agenthub/openai/responses/get_response.py new file mode 100644 index 0000000..a3cc952 --- /dev/null +++ b/examples/agenthub/openai/responses/get_response.py @@ -0,0 +1,25 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates retrieving a response from the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + # Create a response first + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print("Created response ID:", response.id) + + # Retrieve the response by ID + retrieved = client.responses.retrieve(response_id=response.id) + print("Retrieved response:", retrieved.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/image_input_with_base64.py b/examples/agenthub/openai/responses/image_input_with_base64.py new file mode 100644 index 0000000..17a258a --- /dev/null +++ b/examples/agenthub/openai/responses/image_input_with_base64.py @@ -0,0 +1,48 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates providing image input via base64 to the Responses API.""" + +import base64 +from pathlib import Path + +from examples import common + + +def encode_image(image_path): + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + +def main(): + client = common.build_openai_agenthub_client() + + # assuming the file "Cat.jpg" is in the same directory as this script + image_file_path = Path(__file__).parent / "Cat.jpg" + base64_image = encode_image(image_file_path) + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + store=False, + input=[ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What's in this image?", + }, + { + "type": "input_image", + "image_url": f"data:image/jpeg;base64,{base64_image}", + "detail": "high", + }, + ], + } + ], + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/image_input_with_url.py b/examples/agenthub/openai/responses/image_input_with_url.py new file mode 100644 index 0000000..b7bdb55 --- /dev/null +++ b/examples/agenthub/openai/responses/image_input_with_url.py @@ -0,0 +1,35 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates providing image input via URL to the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + store=False, + input=[ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What's in this image?", + }, + { + "type": "input_image", + "image_url": "https://picsum.photos/id/237/200/300", + }, + ], + } + ], + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/reasoning.py b/examples/agenthub/openai/responses/reasoning.py new file mode 100644 index 0000000..45ff65b --- /dev/null +++ b/examples/agenthub/openai/responses/reasoning.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates a reasoning-style Responses API request.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + prompt = """ + Write a bash script that takes a matrix represented as a string with + format '[1,2],[3,4],[5,6]' and prints the transpose in the same format. + """ + + response = client.responses.create( + model="openai.gpt-oss-120b", + input=prompt, + reasoning={"effort": "medium", "summary": "detailed"}, + stream=True, + ) + for event in response: + if event.type == "response.reasoning_summary_part.added": + print("Thinking...") + if event.type == "response.reasoning_summary_text.delta": + print(event.delta, end="", flush=True) + if event.type == "response.output_text.delta": + print(event.delta, end="", flush=True) + print() + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/streaming_text_delta.py b/examples/agenthub/openai/responses/streaming_text_delta.py new file mode 100644 index 0000000..22372c2 --- /dev/null +++ b/examples/agenthub/openai/responses/streaming_text_delta.py @@ -0,0 +1,26 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates streaming Responses API output and handling text deltas.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response_stream = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What are the shapes of OCI GPUs?", + stream=True, + ) + + for event in response_stream: + if event.type == "response.output_text.delta": + print(event.delta, end="", flush=True) + + print() + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/structured_output.py b/examples/agenthub/openai/responses/structured_output.py new file mode 100644 index 0000000..ab8ce9a --- /dev/null +++ b/examples/agenthub/openai/responses/structured_output.py @@ -0,0 +1,41 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates structured output with the Responses API.""" + +from pydantic import BaseModel + +from examples import common + + +class CalendarEvent(BaseModel): + name: str + date: str + participants: list[str] + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.parse( + model="xai.grok-4-1-fast-reasoning", + input=[ + { + "role": "system", + "content": "Extract the event information.", + }, + { + "role": "user", + "content": "Alice and Bob are going to a science fair on Friday.", + }, + ], + store=False, + text_format=CalendarEvent, + ) + + event = response.output_parsed + print(event) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/use_gpt_model.py b/examples/agenthub/openai/responses/use_gpt_model.py new file mode 100644 index 0000000..2a61aaf --- /dev/null +++ b/examples/agenthub/openai/responses/use_gpt_model.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates using a GPT model with the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="openai.gpt-5.2", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/use_gptoss_model_dac.py b/examples/agenthub/openai/responses/use_gptoss_model_dac.py new file mode 100644 index 0000000..c4111eb --- /dev/null +++ b/examples/agenthub/openai/responses/use_gptoss_model_dac.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates using a GPT OSS DAC model with the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py b/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py new file mode 100644 index 0000000..2a4a68d --- /dev/null +++ b/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates using a GPT OSS on-demand model with the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="openai.gpt-oss-120b", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/responses/use_grok_model.py b/examples/agenthub/openai/responses/use_grok_model.py new file mode 100644 index 0000000..067e40d --- /dev/null +++ b/examples/agenthub/openai/responses/use_grok_model.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates using a Grok model with the Responses API.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What is 2x2?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/__init__.py b/examples/agenthub/openai/tools/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/tools/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/tools/code_interpreter.py b/examples/agenthub/openai/tools/code_interpreter.py new file mode 100644 index 0000000..164f11f --- /dev/null +++ b/examples/agenthub/openai/tools/code_interpreter.py @@ -0,0 +1,27 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates the code_interpreter tool in AgentHub.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + tools=[ + { + "type": "code_interpreter", + "container": {"type": "auto", "memory_limit": "4g"}, + } + ], + instructions="Write and run code using the python tool to answer the question.", + input="I need to solve the equation 3x + 11 = 14. Can you help me?", + ) + print(response) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/file_search.py b/examples/agenthub/openai/tools/file_search.py new file mode 100644 index 0000000..9ae5176 --- /dev/null +++ b/examples/agenthub/openai/tools/file_search.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates the file_search tool in AgentHub.""" + +from examples import common + +VECTOR_STORE_ID = "<>" + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input="What are shapes of OCI GPU?", + tools=[ + { + "type": "file_search", + "vector_store_ids": [VECTOR_STORE_ID], + } + ], + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/function_calling.py b/examples/agenthub/openai/tools/function_calling.py new file mode 100644 index 0000000..453a1f9 --- /dev/null +++ b/examples/agenthub/openai/tools/function_calling.py @@ -0,0 +1,84 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates function calling tools in AgentHub.""" + +import json + +from openai.types.responses import ResponseFunctionToolCall +from openai.types.responses.response_input_param import FunctionCallOutput + +from examples import common + +model = "xai.grok-4-1-fast-reasoning" + + +# Define local functions +def get_current_weather(location: str) -> dict: + """Mock weather function.""" + return { + "location": location, + "temperature": "72", + "unit": "fahrenheit", + "forecast": ["sunny", "windy"], + } + + +def main(): + client = common.build_openai_agenthub_client() + + # Define function tool schema + function_tools = [ + { + "type": "function", + "name": "get_current_weather", + "description": "Get current weather for a given location.", + "strict": True, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and country e.g. Bogota, Colombia", + } + }, + "required": ["location"], + "additionalProperties": False, + }, + } + ] + + # First API request - model decides to call the function + response = client.responses.create( + model=model, + input="What is the weather in Seattle?", + tools=function_tools, + ) + print("First response:", response.output) + + # If the model requested a function call, execute it and send the result back + if isinstance(response.output[0], ResponseFunctionToolCall): + tool_call = response.output[0] + function_args = json.loads(tool_call.arguments) + + # Execute the local function + result = get_current_weather(**function_args) + + # Second API request - send the function output back to the model + response = client.responses.create( + model=model, + input=[ + FunctionCallOutput( + type="function_call_output", + call_id=tool_call.call_id, + output=json.dumps(result), + ) + ], + previous_response_id=response.id, + tools=function_tools, + ) + print("Final response:", response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/image_generation.py b/examples/agenthub/openai/tools/image_generation.py new file mode 100644 index 0000000..29bb1bc --- /dev/null +++ b/examples/agenthub/openai/tools/image_generation.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates image generation tooling in AgentHub.""" + +import base64 + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="openai.gpt-5.2", + input="Generate an image of gray tabby cat hugging an otter with an orange scarf", + tools=[{"type": "image_generation"}], + store=False, + stream=False, + ) + + # Save the generated image to a file + image_data = [ + output.result for output in response.output if output.type == "image_generation_call" + ] + + if image_data: + image_base64 = image_data[0] + with open("generated_image.png", "wb") as f: + f.write(base64.b64decode(image_base64)) + print("Image saved to generated_image.png") + else: + print("No image was generated.") + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/multiple_tools.py b/examples/agenthub/openai/tools/multiple_tools.py new file mode 100644 index 0000000..d8f4b81 --- /dev/null +++ b/examples/agenthub/openai/tools/multiple_tools.py @@ -0,0 +1,47 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates using multiple tools in a single request.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response_stream = client.responses.create( + model="openai.gpt-5.1", + tools=[ + {"type": "web_search"}, + { + "type": "mcp", + "server_label": "gitmcp", + "server_url": "https://gitmcp.io/openai/tiktoken", + "allowed_tools": [ + "search_tiktoken_documentation", + "fetch_tiktoken_documentation", + ], + "require_approval": "never", + }, + { + "type": "mcp", + "server_label": "dmcp", + "server_description": "A Dungeons and Dragons MCP server to" + " assist with dice rolling.", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never", + }, + ], + input="What are top news in Seattle today? How does tiktoken work? Roll 2d4+1.", + stream=True, + ) + + for event in response_stream: + if event.type == "response.output_text.delta": + print(event.delta, end="", flush=True) + + print() + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/remote_mcp.py b/examples/agenthub/openai/tools/remote_mcp.py new file mode 100644 index 0000000..2bbc3d6 --- /dev/null +++ b/examples/agenthub/openai/tools/remote_mcp.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates calling a remote MCP tool.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response_stream = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + tools=[ + { + "type": "mcp", + "server_label": "dmcp", + "server_description": "A Dungeons and Dragons MCP server to " + "assist with dice rolling.", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never", + }, + ], + input="Roll 2d4+1", + stream=True, + ) + + for event in response_stream: + if event.type == "response.output_text.delta": + print(event.delta, end="", flush=True) + + print() + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/remote_mcp_approval_flow.py b/examples/agenthub/openai/tools/remote_mcp_approval_flow.py new file mode 100644 index 0000000..b4c3be8 --- /dev/null +++ b/examples/agenthub/openai/tools/remote_mcp_approval_flow.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates an approval flow for remote MCP tools.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + # First API request - Ask the model to call the MCP server, + # and requires your approval to execute the tool call + response1 = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + tools=[ + { + "type": "mcp", + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "always", + }, + ], + input="please tell me structure about facebook/react", + ) + print(response1.output) + + # Find the MCP approval request in the response + approval_request = next( + (item for item in response1.output if item.type == "mcp_approval_request"), None + ) + if not approval_request: + raise ValueError("No MCP approval request found in response") + + # Build your MCP approval response + approval_response = { + "type": "mcp_approval_response", + "approval_request_id": approval_request.id, + "approve": True, + } + + # Second APIrequest - Send the MCP approval response back to the model + response2 = client.responses.create( + model="xai.grok-4-1-fast-reasoning", + input=[approval_response], + tools=[ + { + "type": "mcp", + "server_label": "deepwiki", # this must match the server_label + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "always", + } + ], + previous_response_id=response1.id, + ) + print(response2.output) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/web_search.py b/examples/agenthub/openai/tools/web_search.py new file mode 100644 index 0000000..a79283b --- /dev/null +++ b/examples/agenthub/openai/tools/web_search.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates the web_search tool in AgentHub.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response = client.responses.create( + model="openai.gpt-5.1", + tools=[{"type": "web_search"}], + input="What was a positive news story on 2025-11-14?", + ) + print(response.output_text) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/tools/web_search_streaming.py b/examples/agenthub/openai/tools/web_search_streaming.py new file mode 100644 index 0000000..2214432 --- /dev/null +++ b/examples/agenthub/openai/tools/web_search_streaming.py @@ -0,0 +1,24 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates streaming results from the web_search tool.""" + +from examples import common + + +def main(): + client = common.build_openai_agenthub_client() + + response_stream = client.responses.create( + model="openai.gpt-5.1", + tools=[{"type": "web_search"}], + input="What was a positive news story on 2026-03-06?", + stream=True, + ) + + for event in response_stream: + print(event) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/vector_stores/__init__.py b/examples/agenthub/openai/vector_stores/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/agenthub/openai/vector_stores/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py b/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py new file mode 100644 index 0000000..bd50c32 --- /dev/null +++ b/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates CRUD operations for vector store file batches.""" + +from examples import common + +VECTOR_STORE_ID = "<>" + + +def main(): + client = common.build_openai_agenthub_client() + with open("../files/sample_doc.pdf", "rb") as f1, open("../files/sample_doc.pdf", "rb") as f2: + file_1 = client.files.create( + file=f1, + purpose="user_data", + ) + file_2 = client.files.create( + file=f2, + purpose="user_data", + ) + # Create a batch with file IDs and shared attributes + batch_result = client.vector_stores.file_batches.create( + vector_store_id=VECTOR_STORE_ID, + file_ids=[file_1.id, file_2.id], + attributes={"category": "history"}, + ) + print("Created batch:", batch_result) + + # Retrieve batch status + retrieve_result = client.vector_stores.file_batches.retrieve( + vector_store_id=VECTOR_STORE_ID, + batch_id=batch_result.id, + ) + print("\nBatch status:", retrieve_result) + + # List files in a batch + list_result = client.vector_stores.file_batches.list_files( + vector_store_id=VECTOR_STORE_ID, + batch_id=batch_result.id, + ) + print("\nBatch files:", list_result) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/vector_stores/vector_store_files_crud.py b/examples/agenthub/openai/vector_stores/vector_store_files_crud.py new file mode 100644 index 0000000..67d81ff --- /dev/null +++ b/examples/agenthub/openai/vector_stores/vector_store_files_crud.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates CRUD operations for vector store files.""" + +from examples import common + +VECTOR_STORE_ID = "<>" + + +def main(): + client = common.build_openai_agenthub_client() + # Create the file + with open("../files/sample_doc.pdf", "rb") as f: + file = client.files.create( + file=f, + purpose="user_data", + ) + print(f"Created a File: {file.id}, now waiting for it to get processed") + client.files.wait_for_processing(file.id) + + # Add a file to a vector store + create_result = client.vector_stores.files.create( + vector_store_id=VECTOR_STORE_ID, + file_id=file.id, + attributes={"category": "history"}, + ) + print("\nCreated vector store file:", create_result) + + # List vector store files + list_result = client.vector_stores.files.list( + vector_store_id=VECTOR_STORE_ID, + ) + print("\nFiles:", list_result) + + # Retrieve vector store file + retrieve_result = client.vector_stores.files.retrieve( + vector_store_id=VECTOR_STORE_ID, + file_id=file.id, + ) + print("\nRetrieved:", retrieve_result) + + # Update vector store file attributes + update_result = client.vector_stores.files.update( + vector_store_id=VECTOR_STORE_ID, + file_id=file.id, + attributes={"category": "history", "period": "medieval"}, + ) + print("\nUpdated:", update_result) + + # Delete vector store file (removes parsed content, not the original file) + delete_result = client.vector_stores.files.delete( + vector_store_id=VECTOR_STORE_ID, + file_id=file.id, + ) + print("\nDeleted:", delete_result) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/vector_stores/vector_stores_crud.py b/examples/agenthub/openai/vector_stores/vector_stores_crud.py new file mode 100644 index 0000000..cf36fdf --- /dev/null +++ b/examples/agenthub/openai/vector_stores/vector_stores_crud.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Vector Stores API examples - create, list, retrieve, update, search, and delete.""" + +from examples import common + + +def main(): + cp_client = common.build_openai_agenthub_cp_client() + # Create a vector store + vector_store = cp_client.vector_stores.create( + name="OCI Support FAQ Vector Store", + description="My vector store for supporting customer queries", + expires_after={ + "anchor": "last_active_at", + "days": 30, + }, + metadata={ + "topic": "oci", + }, + ) + print("Created vector store:", vector_store.id) + + # List vector stores + list_result = cp_client.vector_stores.list(limit=20, order="desc") + print("\nVector stores:", list_result) + + # Retrieve vector store + retrieve_result = cp_client.vector_stores.retrieve( + vector_store_id=vector_store.id, + ) + print("\nRetrieved:", retrieve_result) + + # Update vector store + update_result = cp_client.vector_stores.update( + vector_store_id=vector_store.id, + name="Updated Demo Vector Store", + metadata={"category": "history", "period": "medieval"}, + ) + print("\nUpdated:", update_result) + + # Delete vector store + delete_result = cp_client.vector_stores.delete( + vector_store_id=vector_store.id, + ) + print("\nDeleted:", delete_result) + + +if __name__ == "__main__": + main() diff --git a/examples/agenthub/openai/vector_stores/vector_stores_search.py b/examples/agenthub/openai/vector_stores/vector_stores_search.py new file mode 100644 index 0000000..d46871e --- /dev/null +++ b/examples/agenthub/openai/vector_stores/vector_stores_search.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates searching vector stores.""" + +import time + +from examples import common + + +def main(): + dp_client = common.build_openai_agenthub_client() + with open("../files/sample_doc.pdf", "rb") as f: + # Upload a file using the Files API + file = dp_client.files.create( + file=f, + purpose="user_data", + ) + print(f"Uploaded file:{file.id}, waiting for it to be processed") + # dp_client.files.wait_for_processing(file.id) + + cp_client = common.build_openai_agenthub_cp_client() + # Create a vector store + vector_store = cp_client.vector_stores.create( + name="OCI Support FAQ Vector Store", + description="My vector store for supporting customer queries", + expires_after={ + "anchor": "last_active_at", + "days": 30, + }, + ) + print("Created vector store:", vector_store.id) + + # Wait for vector store resource to be in the "completed" state + while True: + vector_store = cp_client.vector_stores.retrieve(vector_store_id=vector_store.id) + print("Vector store status:", vector_store.status) + if vector_store.status == "completed": + break + else: + time.sleep(5) + + # Wait a few more seconds after completed state for the vector store to be fully activated + time.sleep(10) + + # Add a file to a vector store using the Vector Store Files API + create_result = dp_client.vector_stores.files.create( + vector_store_id=vector_store.id, + file_id=file.id, + attributes={"category": "docfiles"}, + ) + print("Created vector store file:", create_result) + + while True: + file_status = dp_client.vector_stores.files.retrieve( + vector_store_id=vector_store.id, + file_id=file.id, + ) + print("Vector store file status:", file_status.status) + if file_status.status == "completed": + break + else: + time.sleep(3) + + # Now the vector store file is indexed, we can search the vector store by a query term + search_results_page = dp_client.vector_stores.search( + vector_store_id=vector_store.id, + query="OCI GenAI Auth", + max_num_results=10, + ) + print("\nSearch results page:", search_results_page) + + if search_results_page.data: + for page_data in search_results_page.data: + print("\nSearch results page data:", page_data) + else: + print("\nNo search results found") + + +if __name__ == "__main__": + main() diff --git a/examples/anthropic/basic_messages.py b/examples/anthropic/basic_messages.py deleted file mode 100644 index fae2ed9..0000000 --- a/examples/anthropic/basic_messages.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from examples import common - -MODEL = "claude-opus-4-6" - - -def main() -> None: - client = common.build_anthropic_client() - - message = client.messages.create( - model=MODEL, - max_tokens=256, - messages=[ - {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} - ], - ) - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/basic_messages_api_key.py b/examples/anthropic/basic_messages_api_key.py deleted file mode 100644 index 29a0bdd..0000000 --- a/examples/anthropic/basic_messages_api_key.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - - -from anthropic import Anthropic - -MODEL = "claude-opus-4-6" - - -def main() -> None: - client = Anthropic( - api_key="<>", - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", - ) - - message = client.messages.create( - model=MODEL, - max_tokens=256, - messages=[ - {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} - ], - ) - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/basic_messages_async.py b/examples/anthropic/basic_messages_async.py deleted file mode 100644 index 95d29bd..0000000 --- a/examples/anthropic/basic_messages_async.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import asyncio - -from examples import common - -MODEL = "claude-opus-4-6" - - -async def main() -> None: - client = common.build_anthropic_async_client() - - message = await client.messages.create( - model=MODEL, - max_tokens=256, - messages=[ - {"role": "user", "content": "Write a one-sentence bedtime story about a unicorn."} - ], - ) - print(message.model_dump_json(indent=2)) - await client.close() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/anthropic/messages_context_management.py b/examples/anthropic/messages_context_management.py deleted file mode 100644 index 250d421..0000000 --- a/examples/anthropic/messages_context_management.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - -MODEL = "claude-opus-4-6" -FILE_PATH = "" - - -def _build_client(): - return common.build_anthropic_client() - - -def main() -> None: - if not FILE_PATH: - raise ValueError( - "Set large FILE_PATH to the document you want to summarize (this will test compaction)." - ) - - client = _build_client() - with open(FILE_PATH) as f: - data = f.read() - print(f"Number of tokens {len(data)/5.0}") - - message = client.beta.messages.create( - model=MODEL, - max_tokens=400, - betas=["compact-2026-01-12"], - context_management={ - "edits": [ - { - "type": "compact_20260112", - "trigger": {"type": "input_tokens", "value": 50000}, - } - ] - }, - messages=[ - { - "role": "user", - "content": f"{data}", - }, - { - "role": "user", - "content": "The above is the research paper, give me 10 main learnings " - "from there. Keep it succint.", - }, - ], - ) - - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_image_text.py b/examples/anthropic/messages_image_text.py deleted file mode 100644 index b1824d8..0000000 --- a/examples/anthropic/messages_image_text.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import base64 -from pathlib import Path - -from examples import common - -MODEL = "claude-opus-4-6" -IMAGE_PATH = Path(__file__).resolve().parents[1] / "oci_openai" / "responses" / "Cat.jpg" - - -def main() -> None: - client = common.build_anthropic_client() - - image_data = base64.b64encode(IMAGE_PATH.read_bytes()).decode("utf-8") - - message = client.messages.create( - model=MODEL, - max_tokens=256, - messages=[ - { - "role": "user", - "content": [ - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": image_data, - }, - }, - {"type": "text", "text": "What's in this image?"}, - ], - } - ], - ) - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_memory.py b/examples/anthropic/messages_memory.py deleted file mode 100644 index 1ca2974..0000000 --- a/examples/anthropic/messages_memory.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from typing import Dict, List - -from examples import common - -MEMORY_ROOT = "/memories" - -MODEL = "claude-opus-4-6" - - -def _build_client(): - return common.build_anthropic_client() - - -def _normalize_path(path: str) -> str | None: - if not path.startswith(MEMORY_ROOT): - return None - parts = [part for part in path.split("/") if part and part != "."] - if ".." in parts: - return None - return "/" + "/".join(parts) - - -def _format_file_listing(memory_files: Dict[str, str]) -> str: - if not memory_files: - return "No stored memories yet." - - lines = ["Stored memories:"] - for filename in sorted(memory_files): - size = len(memory_files[filename].encode("utf-8")) - lines.append(f"- {filename} ({size} bytes)") - return "\n".join(lines) - - -def _format_file_content(file_text: str, view_range: List[int] | None) -> str: - lines = file_text.splitlines() - if view_range and len(view_range) == 2: - start = max(1, view_range[0]) - end = min(len(lines), view_range[1]) - lines = lines[start - 1 : end] - - numbered = [f"{index + 1}: {line}" for index, line in enumerate(lines)] - return "\n".join(numbered) if numbered else "(file is empty)" - - -def _handle_memory_command(memory_files: Dict[str, str], tool_input: dict) -> str: - command = tool_input.get("command") - raw_path = tool_input.get("path", "") - path = _normalize_path(raw_path) - - if not command: - return "Error: missing command in memory tool input." - - if not path: - return f"Error: invalid path '{raw_path}'." - - if command == "view": - if path == MEMORY_ROOT: - return _format_file_listing(memory_files) - if path not in memory_files: - return f"Error: '{path}' not found." - return _format_file_content(memory_files[path], tool_input.get("view_range")) - - if command == "create": - if path in memory_files: - return f"Error: '{path}' already exists." - memory_files[path] = tool_input.get("file_text", "") - return f"Created '{path}'." - - if command == "str_replace": - if path not in memory_files: - return f"Error: '{path}' not found." - old = tool_input.get("old_str", "") - new = tool_input.get("new_str", "") - content = memory_files[path] - count = content.count(old) - if count == 0: - return f"Error: '{old}' not found in '{path}'." - if count > 1: - return f"Error: '{old}' found {count} times in '{path}'." - memory_files[path] = content.replace(old, new, 1) - return f"Updated '{path}'." - - if command == "insert": - if path not in memory_files: - return f"Error: '{path}' not found." - insert_line = tool_input.get("insert_line") - if insert_line is None: - return "Error: insert_line is required for insert." - lines = memory_files[path].splitlines() - index = max(0, min(len(lines), int(insert_line))) - insert_text = tool_input.get("text", "") - lines[index:index] = insert_text.splitlines() or [""] - memory_files[path] = "\n".join(lines) - return f"Inserted text into '{path}'." - - if command == "delete": - if path == MEMORY_ROOT: - memory_files.clear() - return "Deleted all memories." - if path not in memory_files: - return f"Error: '{path}' not found." - memory_files.pop(path) - return f"Deleted '{path}'." - - if command == "rename": - if path not in memory_files: - return f"Error: '{path}' not found." - new_path = _normalize_path(tool_input.get("new_path", "")) - if not new_path: - return "Error: new_path is required for rename." - if new_path in memory_files: - return f"Error: '{new_path}' already exists." - memory_files[new_path] = memory_files.pop(path) - return f"Renamed '{path}' to '{new_path}'." - - return f"Error: unsupported command '{command}'." - - -def main() -> None: - client = _build_client() - memory_files: Dict[str, str] = {} - - messages = [ - { - "role": "user", - "content": "Remember that I prefer concise summaries and my favorite color is green.", - } - ] - - while True: - response = client.beta.messages.create( - model=MODEL, - max_tokens=256, - betas=["context-management-2025-06-27"], - tools=[ - { - "type": "memory_20250818", - "name": "memory", - } - ], - messages=messages, - ) - - print(response.model_dump_json(indent=2)) - - if response.stop_reason != "tool_use": - break - - tool_results = [] - for block in response.content: - if block.type != "tool_use": - continue - if block.name != "memory": - continue - - result = _handle_memory_command(memory_files, block.input) - tool_results.append( - { - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - } - ) - - messages.append({"role": "assistant", "content": response.content}) - messages.append({"role": "user", "content": tool_results}) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_speed_fast.py b/examples/anthropic/messages_speed_fast.py deleted file mode 100644 index da42b05..0000000 --- a/examples/anthropic/messages_speed_fast.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - - -def _build_client(): - return common.build_anthropic_client() - - -MODEL = "claude-opus-4-6" - - -def main() -> None: - client = _build_client() - - message = client.beta.messages.create( - model=MODEL, - max_tokens=200, - betas=["fast-mode-2026-02-01"], - speed="fast", - messages=[ - { - "role": "user", - "content": "List five practical tips for better meeting notes.", - } - ], - ) - - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_streaming.py b/examples/anthropic/messages_streaming.py deleted file mode 100644 index ebb657e..0000000 --- a/examples/anthropic/messages_streaming.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from examples import common - -MODEL = "claude-opus-4-6" - - -def _build_client(): - return common.build_anthropic_client() - - -def main() -> None: - client = _build_client() - - with client.messages.stream( - model=MODEL, - max_tokens=256, - messages=[ - { - "role": "user", - "content": "Write a one-sentence bedtime story about a unicorn.", - } - ], - ) as stream: - for text in stream.text_stream: - print(text, end="", flush=True) - - print() - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_tool_search_client_side.py b/examples/anthropic/messages_tool_search_client_side.py deleted file mode 100644 index 1cc2794..0000000 --- a/examples/anthropic/messages_tool_search_client_side.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - -MODEL = "claude-opus-4-6" - - -def _build_client(): - return common.build_anthropic_client() - - -def _handle_tool_use(block): - if block.name == "client_tool_search": - query = (block.input or {}).get("query", "").lower() - references = [] - if "weather" in query: - references.append({"type": "tool_reference", "tool_name": "get_weather"}) - if not references: - return { - "type": "tool_result", - "tool_use_id": block.id, - "content": "No matching tools found.", - } - return { - "type": "tool_result", - "tool_use_id": block.id, - "content": references, - } - - if block.name == "get_weather": - location = (block.input or {}).get("location", "unknown location") - return { - "type": "tool_result", - "tool_use_id": block.id, - "content": f"Weather in {location}: 62F and sunny.", - } - - return { - "type": "tool_result", - "tool_use_id": block.id, - "content": f"Unhandled tool '{block.name}'.", - } - - -def main() -> None: - client = _build_client() - - tools = [ - { - "name": "client_tool_search", - "description": "Search for a relevant tool by keyword.", - "input_schema": { - "type": "object", - "properties": {"query": {"type": "string"}}, - "required": ["query"], - }, - }, - { - "name": "get_weather", - "description": "Get current weather for a city.", - "defer_loading": True, - "input_schema": { - "type": "object", - "properties": {"location": {"type": "string"}}, - "required": ["location"], - }, - }, - ] - - messages = [ - { - "role": "user", - "content": "What is the weather in Seattle?", - } - ] - - while True: - response = client.messages.create( - model=MODEL, - max_tokens=256, - system="Use client_tool_search to discover tools before calling them.", - messages=messages, - tools=tools, - tool_choice={"type": "auto"}, - ) - - print(response.model_dump_json(indent=2)) - - if response.stop_reason != "tool_use": - break - - tool_results = [] - for block in response.content: - if block.type != "tool_use": - continue - tool_results.append(_handle_tool_use(block)) - - messages.append({"role": "assistant", "content": response.content}) - messages.append({"role": "user", "content": tool_results}) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_web_fetch.py b/examples/anthropic/messages_web_fetch.py deleted file mode 100644 index fcb33df..0000000 --- a/examples/anthropic/messages_web_fetch.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - -MODEL = "claude-opus-4-6" - - -def _build_client(): - return common.build_anthropic_client() - - -def main() -> None: - client = _build_client() - - message = client.beta.messages.create( - model=MODEL, - max_tokens=512, - messages=[ - { - "role": "user", - "content": "Summarize the key points from https://www.anthropic.com" - " in 2-3 sentences.", - } - ], - tools=[ - { - "type": "web_fetch_20250910", - "name": "web_fetch", - "max_uses": 1, - } - ], - betas=["web-fetch-2025-09-10"], - ) - - print(message) - - -if __name__ == "__main__": - main() diff --git a/examples/anthropic/messages_web_search.py b/examples/anthropic/messages_web_search.py deleted file mode 100644 index 6db0b6f..0000000 --- a/examples/anthropic/messages_web_search.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - -MODEL = "claude-opus-4-6" - - -def _build_client(): - return common.build_anthropic_client() - - -def main() -> None: - client = _build_client() - - message = client.messages.create( - model=MODEL, - max_tokens=512, - messages=[ - { - "role": "user", - "content": "Find one recent headline about quantum computing and " - "summarize it in one sentence.", - } - ], - tools=[ - { - "type": "web_search_20250305", - "name": "web_search", - "max_uses": 2, - } - ], - ) - - print(message.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/common.py b/examples/common.py index 6bd28df..377192b 100644 --- a/examples/common.py +++ b/examples/common.py @@ -1,142 +1,84 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ -from __future__ import annotations +"""Demonstrates the Common example.""" -from typing import TYPE_CHECKING +import os import httpx +from oci_genai_support.openai.oci_openai import OPC_COMPARTMENT_ID_HEADER +from openai import AsyncOpenAI, OpenAI from oci_genai_auth import OciSessionAuth -if TYPE_CHECKING: - from anthropic import Anthropic, AsyncAnthropic - from google import genai - from openai import AsyncOpenAI, OpenAI +""" +Common clients used by examples. -# Fill in these values for tests -COMPARTMENT_ID = "" -CONVERSATION_STORE_ID = "" -OPENAI_PROJECT = "" +- AgentHub (non-pass-through) provide build_openai_agenthub_client (DP) + and build_openai_agenthub_cp_client (CP). +- Partner (pass-through) examples use `build_openai_client` helpers. +""" + +# Shared defaults. PROFILE_NAME = "DEFAULT" -GEMINI_API_KEY = "" +COMPARTMENT_ID = "<>" +PROJECT_OCID = "<>" -# OpenAI-compatible base URLs. -OPENAI_BASE_URL_PT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1" -OPENAI_BASE_URL_NP = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1" -# Switch to "NP" for examples that store data on the server. -RESPONSE_API_MODE = "PT" # "PT" (pass-through) or "NP" (non-pass-through) +REGION = "us-chicago-1" -# Other provider base URLs. -ANTHROPIC_BASE_URL = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic" -GOOGLE_BASE_URL = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/google" +# Partner (pass-through) base URL. +PARTNER_OPENAI_BASE_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/v1" +# AgentHub (non-pass-through) base URLs. +AGENTHUB_OPENAI_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/openai/v1" +AGENTHUB_OPENAI_CP_URL = f"https://generativeai.{REGION}.oci.oraclecloud.com/20231130/openai/v1" -def _build_headers(include_conversation_store_id: bool = False) -> dict[str, str]: - headers: dict[str, str] = { - "CompartmentId": COMPARTMENT_ID, - "opc-compartment-id": COMPARTMENT_ID, - "OpenAI-Project": OPENAI_PROJECT, - } - if include_conversation_store_id: - headers["opc-conversation-store-id"] = CONVERSATION_STORE_ID - return {key: value for key, value in headers.items() if value} +def build_openai_agenthub_client(): + return OpenAI( + base_url=AGENTHUB_OPENAI_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + ) -def _resolve_openai_base_url() -> str: - return OPENAI_BASE_URL_NP if RESPONSE_API_MODE == "NP" else OPENAI_BASE_URL_PT +def build_openai_agenthub_cp_client(): + return OpenAI( + base_url=AGENTHUB_OPENAI_CP_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + default_headers={OPC_COMPARTMENT_ID_HEADER: COMPARTMENT_ID}, + ) + + +def build_openai_agenthub_async_client(): + return AsyncOpenAI( + base_url=AGENTHUB_OPENAI_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + http_client=httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + ) -def build_openai_client() -> "OpenAI": - from openai import OpenAI +def build_openai_pt_client() -> OpenAI: client_kwargs = { "api_key": "not-used", - "base_url": _resolve_openai_base_url(), - "http_client": httpx.Client( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=_build_headers(include_conversation_store_id=True), - ), + "base_url": PARTNER_OPENAI_BASE_URL, + "http_client": httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), } - if OPENAI_PROJECT: - client_kwargs["project"] = OPENAI_PROJECT + if COMPARTMENT_ID: + client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} return OpenAI(**client_kwargs) -def build_openai_async_client() -> "AsyncOpenAI": - from openai import AsyncOpenAI - +def build_openai_pt_async_client() -> AsyncOpenAI: client_kwargs = { "api_key": "not-used", - "base_url": _resolve_openai_base_url(), - "http_client": httpx.AsyncClient( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=_build_headers(include_conversation_store_id=True), - ), + "base_url": PARTNER_OPENAI_BASE_URL, + "http_client": httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), } - if OPENAI_PROJECT: - client_kwargs["project"] = OPENAI_PROJECT + if COMPARTMENT_ID: + client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} return AsyncOpenAI(**client_kwargs) - - -def build_anthropic_client() -> "Anthropic": - from anthropic import Anthropic - - return Anthropic( - api_key="not-used", - base_url=ANTHROPIC_BASE_URL, - http_client=httpx.Client( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=_build_headers(), - ), - ) - - -def build_anthropic_async_client() -> "AsyncAnthropic": - from anthropic import AsyncAnthropic - - return AsyncAnthropic( - api_key="not-used", - base_url=ANTHROPIC_BASE_URL, - http_client=httpx.AsyncClient( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=_build_headers(), - ), - ) - - -def build_google_client() -> "genai.Client": - from google import genai - - headers = _build_headers() - http_client = httpx.Client( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=headers, - ) - return genai.Client( - api_key="not-used", - http_options={ - "base_url": GOOGLE_BASE_URL, - "headers": headers, - "httpx_client": http_client, - }, - ) - - -def build_google_async_client() -> tuple["genai.Client", httpx.AsyncClient]: - from google import genai - - headers = _build_headers() - http_client = httpx.AsyncClient( - auth=OciSessionAuth(profile_name=PROFILE_NAME), - headers=headers, - ) - client = genai.Client( - api_key="not-used", - http_options={ - "base_url": GOOGLE_BASE_URL, - "headers": headers, - "httpx_async_client": http_client, - }, - ) - return client, http_client diff --git a/examples/fc_tools.py b/examples/fc_tools.py index ed26a5d..ea9966b 100644 --- a/examples/fc_tools.py +++ b/examples/fc_tools.py @@ -1,6 +1,8 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +"""Demonstrates the Fc tools example.""" + from openai.types.responses.function_tool_param import FunctionToolParam diff --git a/examples/google/generate_content.py b/examples/google/generate_content.py deleted file mode 100644 index 2835f8e..0000000 --- a/examples/google/generate_content.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from examples import common - -MODEL = "google.gemini-2.5-flash" - - -def main() -> None: - client = common.build_google_client() - - response = client.models.generate_content( - model=MODEL, - contents="Write a one-sentence bedtime story about a unicorn.", - ) - print(response.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/google/generate_content_api_key.py b/examples/google/generate_content_api_key.py deleted file mode 100644 index f126e17..0000000 --- a/examples/google/generate_content_api_key.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from google import genai - -MODEL = "google.gemini-2.5-flash" - - -def main() -> None: - client = genai.Client( - api_key="<>", - http_options={ - "base_url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/google" - }, - ) - - response = client.models.generate_content( - model=MODEL, - contents="Write a one-sentence bedtime story about a unicorn.", - ) - print(response.model_dump_json(indent=2)) - - -if __name__ == "__main__": - main() diff --git a/examples/google/generate_content_async.py b/examples/google/generate_content_async.py deleted file mode 100644 index 9a86c65..0000000 --- a/examples/google/generate_content_async.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import asyncio - -from examples import common - -MODEL = "google.gemini-2.5-flash" - - -async def main() -> None: - client, http_client = common.build_google_async_client() - - response = await client.aio.models.generate_content( - model=MODEL, - contents="Write a one-sentence bedtime story about a unicorn.", - ) - print(response.model_dump_json(indent=2)) - await http_client.aclose() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/google/generate_content_streaming.py b/examples/google/generate_content_streaming.py deleted file mode 100644 index 71c039b..0000000 --- a/examples/google/generate_content_streaming.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from __future__ import annotations - -from examples import common - -MODEL = "gemini-2.0-flash-001" - - -def _build_client(): - return common.build_google_client() - - -def main() -> None: - client = _build_client() - - stream = client.models.generate_content_stream( - model=MODEL, - contents="Stream a short poem about the ocean in 4 lines.", - ) - - for chunk in stream: - print(chunk) - - -if __name__ == "__main__": - main() diff --git a/examples/google/generate_images.py b/examples/google/generate_images.py deleted file mode 100644 index 1906af1..0000000 --- a/examples/google/generate_images.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from examples import common - -MODEL = "gemini-3.1-flash-image-preview" - - -def main() -> None: - client = common.build_google_client() - - response = client.models.generate_content( - model=MODEL, - contents=["A poster of a mythical dragon in a neon city."], - ) - for part in response.parts: - if part.text is not None: - print(part.text) - elif part.inline_data is not None: - image = part.as_image() - image.save("generated_image.png") - - -if __name__ == "__main__": - main() diff --git a/examples/openai/chat_completion/basic_chat_completion.py b/examples/openai/chat_completion/basic_chat_completion.py deleted file mode 100644 index ac6b589..0000000 --- a/examples/openai/chat_completion/basic_chat_completion.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -completion = openai_client.chat.completions.create( - model=MODEL, - messages=[ - {"role": "system", "content": "You are a concise assistant."}, - {"role": "user", "content": "List three creative uses for a paperclip."}, - ], - max_tokens=128, -) - -print(completion.model_dump_json(indent=2)) diff --git a/examples/openai/chat_completion/streaming_chat_completion.py b/examples/openai/chat_completion/streaming_chat_completion.py deleted file mode 100644 index 6925671..0000000 --- a/examples/openai/chat_completion/streaming_chat_completion.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -stream = openai_client.chat.completions.create( - model=MODEL, - messages=[ - { - "role": "system", - "content": "You are a concise assistant who answers in one paragraph.", - }, - { - "role": "user", - "content": "Explain why the sky is blue as if you were a physics teacher.", - }, - ], - stream=True, -) - -for chunk in stream: - for choice in chunk.choices: - delta = choice.delta - if delta.content: - print(delta.content, end="", flush=True) -print() diff --git a/examples/openai/chat_completion/tool_call_chat_completion.py b/examples/openai/chat_completion/tool_call_chat_completion.py deleted file mode 100644 index d801b44..0000000 --- a/examples/openai/chat_completion/tool_call_chat_completion.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import json -from typing import Dict - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - - -def get_current_weather(location: str, unit: str = "fahrenheit") -> Dict[str, str]: - # Simple stand-in for a real weather lookup. - return { - "location": location, - "temperature": "72", - "unit": unit, - "forecast": ["sunny", "windy"], - } - - -messages = [ - { - "role": "user", - "content": "What is the weather like in Boston and San Francisco?", - } -] -tools = [ - { - "type": "function", - "function": { - "name": "get_current_weather", - "description": "Get the current weather for a specific location.", - "parameters": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "City and state, for example Boston, MA.", - }, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "Temperature unit to use in the response.", - }, - }, - "required": ["location"], - }, - }, - } -] - -first_response = openai_client.chat.completions.create( - model=MODEL, - messages=messages, - tools=tools, - tool_choice="auto", -) -first_choice = first_response.choices[0] - -if first_choice.finish_reason == "tool_calls": - call_message = first_choice.message - new_messages = messages + [call_message] - for tool_call in call_message.tool_calls: - args = json.loads(tool_call.function.arguments) - tool_result = get_current_weather( - location=args.get("location", ""), - unit=args.get("unit", "fahrenheit"), - ) - new_messages.append( - { - "role": "tool", - "name": tool_call.function.name, - "tool_call_id": tool_call.id, - "content": json.dumps(tool_result), - } - ) - - follow_up = openai_client.chat.completions.create( - model=MODEL, - messages=new_messages, - ) - print(follow_up.choices[0].message.content) -else: - print(first_choice.message.content) diff --git a/examples/openai/conversation_items/create_items.py b/examples/openai/conversation_items/create_items.py deleted file mode 100644 index c8006c0..0000000 --- a/examples/openai/conversation_items/create_items.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -items = openai_client.conversations.items.create( - "conv_977e8f9d624849a79b8eca5e6d67f69a", - items=[ - { - "type": "message", - "role": "user", - "content": [{"type": "input_text", "text": "Hello!"}], - }, - { - "type": "message", - "role": "user", - "content": [{"type": "input_text", "text": "How are you?"}], - }, - ], -) -print(items.data) diff --git a/examples/openai/conversation_items/delete_item.py b/examples/openai/conversation_items/delete_item.py deleted file mode 100644 index 886eb69..0000000 --- a/examples/openai/conversation_items/delete_item.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -conversation = openai_client.conversations.items.delete( - conversation_id="conv_977e8f9d624849a79b8eca5e6d67f69a", - item_id="msg_f7cfcdcf47c944cebb414a495e3c3721", -) -print(conversation) diff --git a/examples/openai/conversation_items/list_items.py b/examples/openai/conversation_items/list_items.py deleted file mode 100644 index 077934c..0000000 --- a/examples/openai/conversation_items/list_items.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -items = openai_client.conversations.items.list("conv_977e8f9d624849a79b8eca5e6d67f69a", limit=10) -print(items.data) diff --git a/examples/openai/conversation_items/retrieve_item.py b/examples/openai/conversation_items/retrieve_item.py deleted file mode 100644 index e7169ff..0000000 --- a/examples/openai/conversation_items/retrieve_item.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -item = openai_client.conversations.items.retrieve( - conversation_id="conv_977e8f9d624849a79b8eca5e6d67f69a", - item_id="msg_f7cfcdcf47c944cebb414a495e3c3721", -) -print(item) diff --git a/examples/openai/conversations/create_conversation.py b/examples/openai/conversations/create_conversation.py deleted file mode 100644 index a2ad31b..0000000 --- a/examples/openai/conversations/create_conversation.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -conversation = openai_client.conversations.create( - metadata={"topic": "demo"}, items=[{"type": "message", "role": "user", "content": "Hello!"}] -) -print(conversation) diff --git a/examples/openai/conversations/delete_conversation.py b/examples/openai/conversations/delete_conversation.py deleted file mode 100644 index 1d4f60e..0000000 --- a/examples/openai/conversations/delete_conversation.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -deleted = openai_client.conversations.delete("conv_b485050b69e54a12ae82cb2688a7217d") -print(deleted) diff --git a/examples/openai/conversations/retrieve_conversation.py b/examples/openai/conversations/retrieve_conversation.py deleted file mode 100644 index 06fb772..0000000 --- a/examples/openai/conversations/retrieve_conversation.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -conversation = openai_client.conversations.retrieve( - "conv_ord_wypqdfsxjfygwh0w4n5w00c3ucomut08y1p5zsogz3o709ug" -) -print(conversation) diff --git a/examples/openai/conversations/update_conversation.py b/examples/openai/conversations/update_conversation.py deleted file mode 100644 index 12009c1..0000000 --- a/examples/openai/conversations/update_conversation.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -updated = openai_client.conversations.update( - "conv_b485050b69e54a12ae82cb2688a7217d", metadata={"topic": "project-x"} -) -print(updated) diff --git a/examples/openai/create_responses_multiturn.py b/examples/openai/create_responses_multiturn.py deleted file mode 100644 index c6a94f8..0000000 --- a/examples/openai/create_responses_multiturn.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - - -def main(): - model = "openai.gpt-4.1" - - # First Request - response1 = openai_client.responses.create( - model=model, - input="Explain what OKRs are in 2 sentences.", - previous_response_id=None, # root of the history - ) - print(response1.output) - - # Second Request with previousResponseId of first request - response2 = openai_client.responses.create( - model=model, - input="Based on that, list 3 common pitfalls to avoid.", - previous_response_id=response1.id, # new branch from response1 - ) - print(response2.output) - - # Second Request with previousResponseId of third request - response3 = openai_client.responses.create( - model=model, - input="Expand bit more on OKRs in a paragraph summary.", - previous_response_id=response1.id, # new branch from response1 - ) - print(response3.output) - - -if __name__ == "__main__": - main() diff --git a/examples/openai/create_responses_websearch.py b/examples/openai/create_responses_websearch.py deleted file mode 100644 index 5041fc8..0000000 --- a/examples/openai/create_responses_websearch.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - - -def main(): - model = "openai.gpt-4.1" - - tools = [ - { - "type": "web_search", - } - ] - - # First Request - response1 = openai_client.responses.create( - model=model, input="please tell me today break news", tools=tools, store=False - ) - print(response1.output) - - -if __name__ == "__main__": - main() diff --git a/examples/openai/create_responses_with_conversation_id.py b/examples/openai/create_responses_with_conversation_id.py deleted file mode 100644 index 3daa864..0000000 --- a/examples/openai/create_responses_with_conversation_id.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - - -def main(): - model = "openai.gpt-4.1" - - conversation = openai_client.conversations.create(metadata={"topic": "demo"}) - print(conversation) - - response = openai_client.responses.create( - model=model, input="Explain what OKRs are in 2 sentences.", conversation=conversation.id - ) - print(response.output) - - response = openai_client.responses.create( - model=model, input="what was my previous question from user?", conversation=conversation.id - ) - print(response.output) - - response = openai_client.responses.create( - model=model, - input="Based on that, list 3 common pitfalls to avoid.", - conversation=conversation.id, - ) - print(response.output) - - -if __name__ == "__main__": - main() diff --git a/examples/openai/function/create_response_fc_parallel_tool.py b/examples/openai/function/create_response_fc_parallel_tool.py deleted file mode 100644 index 2bcf6d6..0000000 --- a/examples/openai/function/create_response_fc_parallel_tool.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common -from examples.fc_tools import fc_tools - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -# parrel_call -response = openai_client.responses.create( - model=MODEL, - input="what is the weather in seattle and in new York?", - previous_response_id=None, # root of the history - tools=fc_tools, -) -print(response.output) - - -# no parrel_call - -response = openai_client.responses.create( - model=MODEL, - input="what is the weather in seattle and in new York?", - previous_response_id=None, # root of the history - tools=fc_tools, - parallel_tool_calls=False, -) -print(response.output) diff --git a/examples/openai/function/create_responses_fc.py b/examples/openai/function/create_responses_fc.py deleted file mode 100644 index ad2e258..0000000 --- a/examples/openai/function/create_responses_fc.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import json - -from openai.types.responses import ResponseFunctionToolCall -from openai.types.responses.response_input_param import FunctionCallOutput -from rich import print - -from examples import common -from examples.fc_tools import execute_function_call, fc_tools - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -# Creates first request -response = openai_client.responses.create( - model=MODEL, - input="what is the weather in seattle?", - previous_response_id=None, # root of the history - tools=fc_tools, -) -print(response.output) - -# Based on output if it is function call, execute the function and provide output back -if isinstance(response.output[0], ResponseFunctionToolCall): - obj = response.output[0] - function_name = obj.name - function_args = json.loads(obj.arguments) - - function_response = execute_function_call(function_name, function_args) - - response = openai_client.responses.create( - model=MODEL, - input=[ - FunctionCallOutput( - type="function_call_output", - call_id=obj.call_id, - output=str(function_response), - ) - ], - previous_response_id=response.id, - tools=fc_tools, - ) - print(response.output) - -# Ask followup question related to previoud context -response = openai_client.responses.create( - model=MODEL, - input="what clothes should i wear in this weather?", - previous_response_id=response.id, - tools=fc_tools, -) -print(response.output) - -# Based on FCTool execute the function tool output -if isinstance(response.output[0], ResponseFunctionToolCall): - obj = response.output[0] - function_name = obj.name - function_args = json.loads(obj.arguments) - - function_response = execute_function_call(function_name, function_args) - - response = openai_client.responses.create( - model=MODEL, - input=[ - FunctionCallOutput( - type="function_call_output", - call_id=obj.call_id, - output=str(function_response), - ) - ], - previous_response_id=response.id, - tools=fc_tools, - ) - print(response.output) diff --git a/examples/openai/mcp/create_response_approval_flow.py b/examples/openai/mcp/create_response_approval_flow.py deleted file mode 100644 index a2928da..0000000 --- a/examples/openai/mcp/create_response_approval_flow.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" -tools = [ - { - "type": "mcp", - "server_label": "deepwiki", - "require_approval": "always", - "server_url": "https://mcp.deepwiki.com/mcp", - } -] -response1 = openai_client.responses.create( - model=MODEL, input="please tell me structure about facebook/react", tools=tools, store=True -) - -print(response1.output) - -approve_id = response1.output[1].id -id = response1.id - -approval_response = { - "type": "mcp_approval_response", - "approval_request_id": approve_id, - "approve": True, -} - - -response2 = openai_client.responses.create( - model=MODEL, input=[approval_response], tools=tools, previous_response_id=id -) -print(response2.output) diff --git a/examples/openai/mcp/create_response_two_mcp.py b/examples/openai/mcp/create_response_two_mcp.py deleted file mode 100644 index ff49a22..0000000 --- a/examples/openai/mcp/create_response_two_mcp.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" -tools = [ - { - "type": "mcp", - "server_label": "stripe", - "require_approval": "never", - "server_url": "https://mcp.stripe.com", - "authorization": "", - }, - { - "type": "mcp", - "server_label": "deepwiki", - "require_approval": "never", - "server_url": "https://mcp.deepwiki.com/mcp", - }, -] -response1 = openai_client.responses.create( - model=MODEL, - input="Please use stirpe create account with a and a@g.com and " - "use deepwiki understand facebook/react", - tools=tools, - store=True, -) - -print(response1.output) diff --git a/examples/openai/mcp/create_responses_mcp.py b/examples/openai/mcp/create_responses_mcp.py deleted file mode 100644 index a2928da..0000000 --- a/examples/openai/mcp/create_responses_mcp.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" -tools = [ - { - "type": "mcp", - "server_label": "deepwiki", - "require_approval": "always", - "server_url": "https://mcp.deepwiki.com/mcp", - } -] -response1 = openai_client.responses.create( - model=MODEL, input="please tell me structure about facebook/react", tools=tools, store=True -) - -print(response1.output) - -approve_id = response1.output[1].id -id = response1.id - -approval_response = { - "type": "mcp_approval_response", - "approval_request_id": approve_id, - "approve": True, -} - - -response2 = openai_client.responses.create( - model=MODEL, input=[approval_response], tools=tools, previous_response_id=id -) -print(response2.output) diff --git a/examples/openai/mcp/create_responses_mcp_auth.py b/examples/openai/mcp/create_responses_mcp_auth.py deleted file mode 100644 index f6335e7..0000000 --- a/examples/openai/mcp/create_responses_mcp_auth.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" -tools = [ - { - "type": "mcp", - "server_label": "stripe", - "require_approval": "never", - "server_url": "https://mcp.stripe.com", - "authorization": "", - } -] -response1 = openai_client.responses.create( - model=MODEL, - input="Please use stirpe create account with a and a@g.com", - tools=tools, - store=True, -) - -print(response1.output) diff --git a/examples/openai/multitools/mcp_function.py b/examples/openai/multitools/mcp_function.py deleted file mode 100644 index 1258fa8..0000000 --- a/examples/openai/multitools/mcp_function.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - - -tools = [ - { - "type": "mcp", - "server_label": "deepwiki", - "require_approval": "never", - "server_url": "https://mcp.deepwiki.com/mcp", - }, - { - "type": "function", - "name": "get_weather", - "description": "function to get_weather", - "parameters": { - "type": "object", - "properties": { - "repoName": {"type": "string", "description": "city or country (e.g. seattle"} - }, - "required": ["repoName"], - "additionalProperties": False, - }, - }, -] - -messages = [ - { - "role": "user", - "content": "please tell me something about facebook/react and get weather for seattle", - } -] - - -# parrel_call -response = openai_client.responses.create(model=MODEL, input=messages, tools=tools) -print(response.output) diff --git a/examples/openai/multitools/mcp_websearch.py b/examples/openai/multitools/mcp_websearch.py deleted file mode 100644 index 9ebfc96..0000000 --- a/examples/openai/multitools/mcp_websearch.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - - -tools = [ - { - "type": "mcp", - "server_label": "deepwiki", - "require_approval": "never", - "server_url": "https://mcp.deepwiki.com/mcp", - }, - { - "type": "web_search", - }, -] - - -# parrel_call -response = openai_client.responses.create( - model=MODEL, - input="search latest repo related to react, and use deepwiki tell me repo structure", - tools=tools, -) -print(response.output) diff --git a/examples/openai/oci_openai_responses_example.py b/examples/openai/oci_openai_responses_example.py deleted file mode 100644 index 9edab76..0000000 --- a/examples/openai/oci_openai_responses_example.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -# mypy: ignore-errors -from examples import common - -MODEL = "openai.gpt-4o" - -PROMPT = "Tell me a three sentence bedtime story about a unicorn." - - -def get_oci_openai_client(): - return common.build_openai_client() - - -def main(): - client = get_oci_openai_client() - response = client.responses.create(model=MODEL, input=PROMPT) - print(response.output[0].content[0].text) - - -if __name__ == "__main__": - main() diff --git a/examples/openai/responses/create_response.py b/examples/openai/responses/create_response.py deleted file mode 100644 index 8f6e77c..0000000 --- a/examples/openai/responses/create_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -model = "openai.gpt-5" - -# First Request -response1 = openai_client.responses.create( - model=model, - input="Explain what OKRs are in 2 sentences.", - previous_response_id=None, # root of the history -) -print(response1) diff --git a/examples/openai/responses/create_response_streaming.py b/examples/openai/responses/create_response_streaming.py deleted file mode 100644 index 34ea28b..0000000 --- a/examples/openai/responses/create_response_streaming.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -model = "openai.gpt-5" - -# Streaming request -stream = openai_client.responses.create( - model=model, - input="Explain what OKRs are in 2 sentences.", - previous_response_id=None, - stream=True, -) - -# Process the stream -print("Streaming response:") -for chunk in stream: - print(chunk) - -print("\n") # New line after streaming completes diff --git a/examples/openai/responses/create_response_with_image_input.py b/examples/openai/responses/create_response_with_image_input.py deleted file mode 100644 index 1e12eb9..0000000 --- a/examples/openai/responses/create_response_with_image_input.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -import base64 - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -model = "openai.gpt-5" - -# Read and encode image -with open("../responses/Cat.jpg", "rb") as image_file: - image_data = base64.b64encode(image_file.read()).decode("utf-8") - - response1 = openai_client.responses.create( - model=model, - input=[ - { - "role": "user", - "content": [ - {"type": "input_text", "text": "what's in this image?"}, - { - "type": "input_image", - "image_url": f"data:image/jpeg;base64,{image_data}", - }, - ], - } - ], - ) - - print(response1.output) diff --git a/examples/openai/responses/delete_response.py b/examples/openai/responses/delete_response.py deleted file mode 100644 index 8cc03ed..0000000 --- a/examples/openai/responses/delete_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -deleted_response = openai_client.responses.delete( - response_id="resp_ord_s4zon7gfqpfub4czrdqqzh217snndlbuf5vxen9gvjv7tqnq" -) -print(deleted_response) diff --git a/examples/openai/responses/get_response.py b/examples/openai/responses/get_response.py deleted file mode 100644 index 5030c5c..0000000 --- a/examples/openai/responses/get_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -retrieved_response = openai_client.responses.retrieve( - response_id="resp_ord_yp5uu39vlnur5hdndgpsdqjp40eiurzij5lsrbu9z4cidb4l" -) -print(retrieved_response) diff --git a/examples/openai/responses/list_input_items.py b/examples/openai/responses/list_input_items.py deleted file mode 100644 index 3e708aa..0000000 --- a/examples/openai/responses/list_input_items.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -response = openai_client.responses.input_items.list( - "resp_ord_yp5uu39vlnur5hdndgpsdqjp40eiurzij5lsrbu9z4cidb4l" -) -print(response) diff --git a/examples/openai/websearch/create_responses_websearch.py b/examples/openai/websearch/create_responses_websearch.py deleted file mode 100644 index 13e9024..0000000 --- a/examples/openai/websearch/create_responses_websearch.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -tools = [ - { - "type": "web_search", - } -] - - -# First Request -response1 = openai_client.responses.create( - model=MODEL, input="please tell me today break news", tools=tools, store=False -) -print(response1.output) diff --git a/examples/openai/websearch/create_responses_websearch_source.py b/examples/openai/websearch/create_responses_websearch_source.py deleted file mode 100644 index c7583b6..0000000 --- a/examples/openai/websearch/create_responses_websearch_source.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -from rich import print - -from examples import common - -openai_client = common.build_openai_client() - -MODEL = "openai.gpt-4.1" - -tools = [ - { - "type": "web_search", - } -] - - -# First Request -response1 = openai_client.responses.create( - model=MODEL, - input="please tell me today break news", - tools=tools, - store=False, - include=["web_search_call.action.sources"], -) -print(response1.output) diff --git a/examples/partner/__init__.py b/examples/partner/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/partner/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/partner/openai/__init__.py b/examples/partner/openai/__init__.py new file mode 100644 index 0000000..b38e643 --- /dev/null +++ b/examples/partner/openai/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ diff --git a/examples/partner/openai/basic_chat_completion.py b/examples/partner/openai/basic_chat_completion.py new file mode 100644 index 0000000..0ce7b6f --- /dev/null +++ b/examples/partner/openai/basic_chat_completion.py @@ -0,0 +1,29 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates a basic chat completion request for the Partner (pass-through) endpoint.""" + +from rich import print + +from examples import common + +MODEL = "openai.gpt-4.1" + + +def main(): + openai_client = common.build_openai_pt_client() + + completion = openai_client.chat.completions.create( + model=MODEL, + messages=[ + {"role": "system", "content": "You are a concise assistant."}, + {"role": "user", "content": "List three creative uses for a paperclip."}, + ], + max_tokens=128, + ) + + print(completion.model_dump_json(indent=2)) + + +if __name__ == "__main__": + main() diff --git a/examples/openai/chat_completion/basic_chat_completion_api_key.py b/examples/partner/openai/basic_chat_completion_api_key.py similarity index 84% rename from examples/openai/chat_completion/basic_chat_completion_api_key.py rename to examples/partner/openai/basic_chat_completion_api_key.py index 035326f..02f6fff 100644 --- a/examples/openai/chat_completion/basic_chat_completion_api_key.py +++ b/examples/partner/openai/basic_chat_completion_api_key.py @@ -1,6 +1,9 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +"""Demonstrates the Basic chat completion api key example.""" + +import os from openai import OpenAI @@ -9,8 +12,8 @@ def main() -> None: client = OpenAI( - api_key="<>", - base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", + api_key=os.getenv("OPENAI_API_KEY"), + base_url="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1", ) response = client.chat.completions.create( diff --git a/examples/openai/oci_openai_chat_completions_example.py b/examples/partner/openai/quickstart_openai_chat_completions.py similarity index 90% rename from examples/openai/oci_openai_chat_completions_example.py rename to examples/partner/openai/quickstart_openai_chat_completions.py index bc57e01..8335bc6 100755 --- a/examples/openai/oci_openai_chat_completions_example.py +++ b/examples/partner/openai/quickstart_openai_chat_completions.py @@ -1,6 +1,8 @@ # Copyright (c) 2026 Oracle and/or its affiliates. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +"""Demonstrates a simple openai chat completions example.""" + import logging from examples import common @@ -9,7 +11,7 @@ def main(): - client = common.build_openai_client() + client = common.build_openai_pt_client() model = "meta.llama-4-scout-17b-16e-instruct" completion = client.chat.completions.create( diff --git a/examples/partner/openai/streaming_chat_completion.py b/examples/partner/openai/streaming_chat_completion.py new file mode 100644 index 0000000..ab2ee87 --- /dev/null +++ b/examples/partner/openai/streaming_chat_completion.py @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates streaming chat completion responses for the Partner (pass-through) endpoint.""" + +from examples import common + +MODEL = "openai.gpt-4.1" + + +def main(): + openai_client = common.build_openai_pt_client() + + stream = openai_client.chat.completions.create( + model=MODEL, + messages=[ + { + "role": "system", + "content": "You are a concise assistant who answers in one paragraph.", + }, + { + "role": "user", + "content": "Explain why the sky is blue as if you were a physics teacher.", + }, + ], + stream=True, + ) + + for chunk in stream: + for choice in chunk.choices: + delta = choice.delta + if delta.content: + print(delta.content, end="", flush=True) + print() + + +if __name__ == "__main__": + main() diff --git a/examples/partner/openai/tool_call_chat_completion.py b/examples/partner/openai/tool_call_chat_completion.py new file mode 100644 index 0000000..3456093 --- /dev/null +++ b/examples/partner/openai/tool_call_chat_completion.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Demonstrates tool calling with chat completions for the Partner (pass-through) endpoint.""" + +import json +from typing import Dict + +from examples import common + +MODEL = "openai.gpt-4.1" + + +def get_current_weather(location: str, unit: str = "fahrenheit") -> Dict[str, str]: + # Simple stand-in for a real weather lookup. + return { + "location": location, + "temperature": "72", + "unit": unit, + "forecast": ["sunny", "windy"], + } + + +def main(): + openai_client = common.build_openai_pt_client() + + messages = [ + { + "role": "user", + "content": "What is the weather like in Boston and San Francisco?", + } + ] + tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather for a specific location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and state, for example Boston, MA.", + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "Temperature unit to use in the response.", + }, + }, + "required": ["location"], + }, + }, + } + ] + + first_response = openai_client.chat.completions.create( + model=MODEL, + messages=messages, + tools=tools, + tool_choice="auto", + ) + first_choice = first_response.choices[0] + + if first_choice.finish_reason == "tool_calls": + call_message = first_choice.message + new_messages = messages + [call_message] + for tool_call in call_message.tool_calls: + args = json.loads(tool_call.function.arguments) + tool_result = get_current_weather( + location=args.get("location", ""), + unit=args.get("unit", "fahrenheit"), + ) + new_messages.append( + { + "role": "tool", + "name": tool_call.function.name, + "tool_call_id": tool_call.id, + "content": json.dumps(tool_result), + } + ) + + follow_up = openai_client.chat.completions.create( + model=MODEL, + messages=new_messages, + ) + print(follow_up.choices[0].message.content) + else: + print(first_choice.message.content) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index db4010b..0a1021f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,8 +48,6 @@ dev = [ "respx", "typing-extensions>=4.11, <5", "openai>=1.108.1", - "google-genai>=1.0.0", - "anthropic>=0.79.0", "openai-agents>=0.5.1", "pytest-mock", "ag2[openai]", diff --git a/src/oci_genai_auth/auth.py b/src/oci_genai_auth/auth.py index 7e0dc44..b19475b 100644 --- a/src/oci_genai_auth/auth.py +++ b/src/oci_genai_auth/auth.py @@ -81,13 +81,11 @@ def _refresh_if_needed(self) -> OciAuthSigner: self._refresh_signer() self._last_refresh = time.time() logger.info("%s token refresh completed successfully", self.__class__.__name__) - except Exception as e: + except Exception: logger.exception("Token refresh failed") return self.signer - def _sign_request( - self, request: httpx.Request, content: bytes, signer: OciAuthSigner - ) -> None: + def _sign_request(self, request: httpx.Request, content: bytes, signer: OciAuthSigner) -> None: """ Sign the given HTTPX request with the OCI signer using the provided content. Updates request.headers in place with the signed headers. @@ -95,14 +93,6 @@ def _sign_request( # Strip any SDK auth headers to avoid conflicting with OCI signing. request.headers.pop("Authorization", None) request.headers.pop("X-Api-Key", None) - request.headers.pop("x-goog-api-key", None) - - # Remove Google API key query parameter if present. - params = list(request.url.params.multi_items()) - if params: - filtered = [(key, value) for key, value in params if key.lower() != "key"] - if len(filtered) != len(params): - request.url = request.url.copy_with(params=filtered) req = requests.Request( method=request.method, url=str(request.url), @@ -151,7 +141,7 @@ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Re signer = self.signer self._sign_request(request, content, signer) yield request - except Exception as e: + except Exception: logger.exception("Token refresh on 401 failed") diff --git a/tests/conftest.py b/tests/conftest.py index 905a75c..06fcc1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def _disable_openai_agents_tracing(): os.environ.setdefault("OPENAI_AGENTS_DISABLE_TRACING", "true") try: from agents.tracing import set_tracing_disabled - except Exception: + except (ImportError, ModuleNotFoundError): yield return set_tracing_disabled(True) diff --git a/tests/test_auth.py b/tests/test_auth.py index 83abb84..f9e8784 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -43,19 +43,16 @@ def test_auth_flow_signs_request(): auth = _DummyAuth(_DummySigner("signed-0")) request = httpx.Request( "GET", - "https://example.com?key=secret&foo=bar", + "https://example.com?foo=bar", headers={ "Authorization": "Bearer test", "X-Api-Key": "api-key", - "x-goog-api-key": "google-key", }, ) flow = auth.auth_flow(request) signed_request = next(flow) assert signed_request.headers["authorization"] == "signed-0" assert "x-api-key" not in signed_request.headers - assert "x-goog-api-key" not in signed_request.headers - assert "key" not in signed_request.url.params assert signed_request.url.params.get("foo") == "bar" @@ -85,9 +82,7 @@ def test_refresh_failure_does_not_break_auth_flow(caplog): signed_request = next(flow) assert signed_request.headers["authorization"] == "signed-0" - assert any( - "Token refresh failed" in record.message for record in caplog.records - ) + assert any("Token refresh failed" in record.message for record in caplog.records) def test_session_auth_initializes_signer_from_config(): @@ -98,11 +93,12 @@ def test_session_auth_initializes_signer_from_config(): "user": "dummy_user", "fingerprint": "dummy_fingerprint", } - with patch("oci.config.from_file", return_value=config), patch( - "oci.signer.load_private_key_from_file", return_value="dummy_private_key" - ), patch("oci.auth.signers.SecurityTokenSigner") as mock_signer, patch( - "builtins.open", create=True - ) as mock_open: + with ( + patch("oci.config.from_file", return_value=config), + patch("oci.signer.load_private_key_from_file", return_value="dummy_private_key"), + patch("oci.auth.signers.SecurityTokenSigner") as mock_signer, + patch("builtins.open", create=True) as mock_open, + ): mock_open.return_value.__enter__.return_value.read.return_value = "dummy_token" auth = OciSessionAuth( profile_name="DEFAULT", @@ -126,9 +122,11 @@ def test_user_principal_auth_uses_signer_from_config(): "user": "dummy_user", "fingerprint": "dummy_fingerprint", } - with patch("oci.config.from_file", return_value=config), patch( - "oci.config.validate_config", return_value=True - ), patch("oci.signer.Signer") as mock_signer: + with ( + patch("oci.config.from_file", return_value=config), + patch("oci.config.validate_config", return_value=True), + patch("oci.signer.Signer") as mock_signer, + ): auth = OciUserPrincipalAuth(profile_name="DEFAULT") mock_signer.assert_called_once() From a2468abf0b0e0deeed09db650546777d237f54b2 Mon Sep 17 00:00:00 2001 From: Varun Shenoy Date: Fri, 20 Mar 2026 01:52:08 -0700 Subject: [PATCH 4/4] Addressing Jing's feedback on the PR. --- README.md | 2 +- .../openai/agents/basic_agents_example.py | 4 +- examples/agenthub/openai/common.py | 50 +++++++++++ .../converstions/conversation_items_crud.py | 4 +- .../openai/converstions/conversations_crud.py | 4 +- examples/agenthub/openai/files/files_crud.py | 4 +- .../create_response_fc_parallel_tool.py | 4 +- .../openai/function/create_responses_fc.py | 4 +- .../mcp/create_response_approval_flow.py | 4 +- .../openai/mcp/create_response_two_mcp.py | 4 +- .../openai/mcp/create_responses_mcp.py | 4 +- .../openai/mcp/create_responses_mcp_auth.py | 4 +- .../openai/memory/long_term_memory.py | 4 +- .../memory/long_term_memory_access_policy.py | 4 +- .../memory/short_term_memory_optimization.py | 4 +- .../openai/multiturn/conversations_api.py | 4 +- .../openai/multiturn/responses_chaining.py | 4 +- .../quickstart_responses_create_api_key.py | 2 +- .../quickstart_responses_create_oci_iam.py | 4 +- .../openai/responses/create_response.py | 4 +- .../responses/create_response_store_false.py | 4 +- .../openai/responses/delete_response.py | 4 +- .../responses/file_input_with_file_id.py | 4 +- .../responses/file_input_with_file_url.py | 4 +- .../agenthub/openai/responses/get_response.py | 4 +- .../responses/image_input_with_base64.py | 4 +- .../openai/responses/image_input_with_url.py | 4 +- .../agenthub/openai/responses/reasoning.py | 4 +- .../openai/responses/streaming_text_delta.py | 4 +- .../openai/responses/structured_output.py | 4 +- .../openai/responses/use_gpt_model.py | 4 +- .../openai/responses/use_gptoss_model_dac.py | 4 +- .../responses/use_gptoss_model_ondemand.py | 4 +- .../openai/responses/use_grok_model.py | 4 +- .../agenthub/openai/tools/code_interpreter.py | 4 +- examples/agenthub/openai/tools/file_search.py | 4 +- .../agenthub/openai/tools/function_calling.py | 4 +- .../agenthub/openai/tools/image_generation.py | 4 +- .../agenthub/openai/tools/multiple_tools.py | 4 +- examples/agenthub/openai/tools/remote_mcp.py | 4 +- .../openai/tools/remote_mcp_approval_flow.py | 4 +- examples/agenthub/openai/tools/web_search.py | 4 +- .../openai/tools/web_search_streaming.py | 4 +- .../vector_store_file_batches_crud.py | 4 +- .../vector_stores/vector_store_files_crud.py | 4 +- .../vector_stores/vector_stores_crud.py | 4 +- .../vector_stores/vector_stores_search.py | 6 +- examples/common.py | 84 ------------------- .../partner/openai/basic_chat_completion.py | 2 +- examples/partner/openai/common.py | 39 +++++++++ .../quickstart_openai_chat_completions.py | 2 +- .../openai/streaming_chat_completion.py | 2 +- .../openai/tool_call_chat_completion.py | 2 +- 53 files changed, 184 insertions(+), 179 deletions(-) create mode 100644 examples/agenthub/openai/common.py delete mode 100644 examples/common.py create mode 100644 examples/partner/openai/common.py diff --git a/README.md b/README.md index fa1cdec..e2a8b91 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ client = OpenAI( ## Running the Examples -1. Update `examples/common.py` with your `COMPARTMENT_ID`, `PROJECT_OCID`, and set the correct `REGION`. +1. Update `examples/agenthub/openai/common.py` and/or `examples/partner/openai/common.py` with your `COMPARTMENT_ID`, `PROJECT_OCID`, and set the correct `REGION`. 2. Set the `OPENAI_API_KEY` environment variable when an example uses API key authentication. 3. Install optional dev dependencies: `pip install -e '.[dev]'`. diff --git a/examples/agenthub/openai/agents/basic_agents_example.py b/examples/agenthub/openai/agents/basic_agents_example.py index ac611a4..144389a 100644 --- a/examples/agenthub/openai/agents/basic_agents_example.py +++ b/examples/agenthub/openai/agents/basic_agents_example.py @@ -8,12 +8,12 @@ from agents import Agent, Runner, set_default_openai_client, trace -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-4o" # Set the OCI OpenAI Client as the default client to use with OpenAI Agents -set_default_openai_client(common.build_openai_agenthub_async_client()) +set_default_openai_client(common.build_agenthub_async_client()) async def main(): diff --git a/examples/agenthub/openai/common.py b/examples/agenthub/openai/common.py new file mode 100644 index 0000000..67b18ea --- /dev/null +++ b/examples/agenthub/openai/common.py @@ -0,0 +1,50 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""AgentHub example clients and configuration.""" + +from __future__ import annotations + +import os + +import httpx +from openai import AsyncOpenAI, OpenAI + +from oci_genai_auth import OciSessionAuth + +# Shared defaults. +PROFILE_NAME = "DEFAULT" +COMPARTMENT_ID = "<>" +PROJECT_OCID = "<>" +REGION = "us-chicago-1" + +AGENTHUB_OPENAI_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/openai/v1" +AGENTHUB_OPENAI_CP_URL = f"https://generativeai.{REGION}.oci.oraclecloud.com/20231130/openai/v1" + + +def build_agenthub_client() -> OpenAI: + return OpenAI( + base_url=AGENTHUB_OPENAI_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + ) + + +def build_agenthub_async_client() -> AsyncOpenAI: + return AsyncOpenAI( + base_url=AGENTHUB_OPENAI_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + http_client=httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + ) + + +def build_agenthub_cp_client() -> OpenAI: + return OpenAI( + base_url=AGENTHUB_OPENAI_CP_URL, + api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), + http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), + default_headers={"opc-compartment-id": COMPARTMENT_ID}, + ) \ No newline at end of file diff --git a/examples/agenthub/openai/converstions/conversation_items_crud.py b/examples/agenthub/openai/converstions/conversation_items_crud.py index cbe405d..0b1e210 100644 --- a/examples/agenthub/openai/converstions/conversation_items_crud.py +++ b/examples/agenthub/openai/converstions/conversation_items_crud.py @@ -3,12 +3,12 @@ """Demonstrates CRUD operations for conversation items in AgentHub.""" -from examples import common +from examples.agenthub.openai import common def main(): # Create an empty conversation - cp_client = common.build_openai_agenthub_client() + cp_client = common.build_agenthub_client() conversation = cp_client.conversations.create() print("\nCreated conversation:", conversation) diff --git a/examples/agenthub/openai/converstions/conversations_crud.py b/examples/agenthub/openai/converstions/conversations_crud.py index bde95a7..25065b3 100644 --- a/examples/agenthub/openai/converstions/conversations_crud.py +++ b/examples/agenthub/openai/converstions/conversations_crud.py @@ -3,11 +3,11 @@ """Demonstrates CRUD operations for conversations in AgentHub.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create a conversation conversation = client.conversations.create( diff --git a/examples/agenthub/openai/files/files_crud.py b/examples/agenthub/openai/files/files_crud.py index ce1710f..e0c97a7 100644 --- a/examples/agenthub/openai/files/files_crud.py +++ b/examples/agenthub/openai/files/files_crud.py @@ -5,11 +5,11 @@ from pathlib import Path -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # List files in the project files_list = client.files.list(order="asc") for file in files_list.data: diff --git a/examples/agenthub/openai/function/create_response_fc_parallel_tool.py b/examples/agenthub/openai/function/create_response_fc_parallel_tool.py index 8828ba7..f5b3fae 100644 --- a/examples/agenthub/openai/function/create_response_fc_parallel_tool.py +++ b/examples/agenthub/openai/function/create_response_fc_parallel_tool.py @@ -5,14 +5,14 @@ from rich import print -from examples import common +from examples.agenthub.openai import common from examples.fc_tools import fc_tools MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() # parrel_call response = openai_client.responses.create( model=MODEL, diff --git a/examples/agenthub/openai/function/create_responses_fc.py b/examples/agenthub/openai/function/create_responses_fc.py index 5d7a0e5..5de83d3 100644 --- a/examples/agenthub/openai/function/create_responses_fc.py +++ b/examples/agenthub/openai/function/create_responses_fc.py @@ -9,14 +9,14 @@ from openai.types.responses.response_input_param import FunctionCallOutput from rich import print -from examples import common +from examples.agenthub.openai import common from examples.fc_tools import execute_function_call, fc_tools MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() # Creates first request response = openai_client.responses.create( diff --git a/examples/agenthub/openai/mcp/create_response_approval_flow.py b/examples/agenthub/openai/mcp/create_response_approval_flow.py index deb18f2..4e248d4 100644 --- a/examples/agenthub/openai/mcp/create_response_approval_flow.py +++ b/examples/agenthub/openai/mcp/create_response_approval_flow.py @@ -5,13 +5,13 @@ from rich import print -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() tools = [ { diff --git a/examples/agenthub/openai/mcp/create_response_two_mcp.py b/examples/agenthub/openai/mcp/create_response_two_mcp.py index 6e043fa..4564d53 100644 --- a/examples/agenthub/openai/mcp/create_response_two_mcp.py +++ b/examples/agenthub/openai/mcp/create_response_two_mcp.py @@ -5,13 +5,13 @@ from rich import print -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() tools = [ { diff --git a/examples/agenthub/openai/mcp/create_responses_mcp.py b/examples/agenthub/openai/mcp/create_responses_mcp.py index ad205ba..469fcd6 100644 --- a/examples/agenthub/openai/mcp/create_responses_mcp.py +++ b/examples/agenthub/openai/mcp/create_responses_mcp.py @@ -5,13 +5,13 @@ from rich import print -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() tools = [ { diff --git a/examples/agenthub/openai/mcp/create_responses_mcp_auth.py b/examples/agenthub/openai/mcp/create_responses_mcp_auth.py index 854394e..42b3a31 100644 --- a/examples/agenthub/openai/mcp/create_responses_mcp_auth.py +++ b/examples/agenthub/openai/mcp/create_responses_mcp_auth.py @@ -5,13 +5,13 @@ from rich import print -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-4.1" def main(): - openai_client = common.build_openai_agenthub_client() + openai_client = common.build_agenthub_client() tools = [ { diff --git a/examples/agenthub/openai/memory/long_term_memory.py b/examples/agenthub/openai/memory/long_term_memory.py index 0bc99f9..82d63c1 100644 --- a/examples/agenthub/openai/memory/long_term_memory.py +++ b/examples/agenthub/openai/memory/long_term_memory.py @@ -5,13 +5,13 @@ import time -from examples import common +from examples.agenthub.openai import common MODEL = "xai.grok-4-1-fast-reasoning" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # First conversation - store preferences conversation1 = client.conversations.create( metadata={"memory_subject_id": "user_123456"}, diff --git a/examples/agenthub/openai/memory/long_term_memory_access_policy.py b/examples/agenthub/openai/memory/long_term_memory_access_policy.py index 958e985..4f64c65 100644 --- a/examples/agenthub/openai/memory/long_term_memory_access_policy.py +++ b/examples/agenthub/openai/memory/long_term_memory_access_policy.py @@ -5,13 +5,13 @@ import time -from examples import common +from examples.agenthub.openai import common MODEL = "openai.gpt-5.1" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # First conversation - store only (no recall) conversation1 = client.conversations.create( metadata={ diff --git a/examples/agenthub/openai/memory/short_term_memory_optimization.py b/examples/agenthub/openai/memory/short_term_memory_optimization.py index d1a3bde..c201727 100644 --- a/examples/agenthub/openai/memory/short_term_memory_optimization.py +++ b/examples/agenthub/openai/memory/short_term_memory_optimization.py @@ -3,13 +3,13 @@ """Demonstrates short-term memory optimization in AgentHub.""" -from examples import common +from examples.agenthub.openai import common MODEL = "xai.grok-4-1-fast-reasoning" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create a conversation with STMO enabled conversation = client.conversations.create( metadata={"topic": "demo", "short_term_memory_optimization": "True"}, diff --git a/examples/agenthub/openai/multiturn/conversations_api.py b/examples/agenthub/openai/multiturn/conversations_api.py index 5a8633f..df9ea79 100644 --- a/examples/agenthub/openai/multiturn/conversations_api.py +++ b/examples/agenthub/openai/multiturn/conversations_api.py @@ -3,13 +3,13 @@ """Demonstrates a multi-turn flow using the Conversations API.""" -from examples import common +from examples.agenthub.openai import common MODEL = "xai.grok-4-1-fast-reasoning" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create a conversation upfront conversation = client.conversations.create(metadata={"topic": "demo"}) diff --git a/examples/agenthub/openai/multiturn/responses_chaining.py b/examples/agenthub/openai/multiturn/responses_chaining.py index ce568ee..d78af26 100644 --- a/examples/agenthub/openai/multiturn/responses_chaining.py +++ b/examples/agenthub/openai/multiturn/responses_chaining.py @@ -3,13 +3,13 @@ """Demonstrates chaining responses across multiple turns.""" -from examples import common +from examples.agenthub.openai import common model = "xai.grok-4-1-fast-reasoning" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # First turn response1 = client.responses.create( model=model, diff --git a/examples/agenthub/openai/quickstart_responses_create_api_key.py b/examples/agenthub/openai/quickstart_responses_create_api_key.py index 75cc908..10c53f3 100644 --- a/examples/agenthub/openai/quickstart_responses_create_api_key.py +++ b/examples/agenthub/openai/quickstart_responses_create_api_key.py @@ -16,7 +16,7 @@ from openai import OpenAI -from examples.common import PROJECT_OCID +from examples.agenthub.openai.common import PROJECT_OCID def main(): diff --git a/examples/agenthub/openai/quickstart_responses_create_oci_iam.py b/examples/agenthub/openai/quickstart_responses_create_oci_iam.py index 02b3ae6..e3494a4 100644 --- a/examples/agenthub/openai/quickstart_responses_create_oci_iam.py +++ b/examples/agenthub/openai/quickstart_responses_create_oci_iam.py @@ -11,11 +11,11 @@ 3. Run this script """ -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/create_response.py b/examples/agenthub/openai/responses/create_response.py index c6ec7ab..c44bafc 100644 --- a/examples/agenthub/openai/responses/create_response.py +++ b/examples/agenthub/openai/responses/create_response.py @@ -3,11 +3,11 @@ """Demonstrates creating a response with the Responses API on AgentHub.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/create_response_store_false.py b/examples/agenthub/openai/responses/create_response_store_false.py index 50e6f96..0b10267 100644 --- a/examples/agenthub/openai/responses/create_response_store_false.py +++ b/examples/agenthub/openai/responses/create_response_store_false.py @@ -3,11 +3,11 @@ """Demonstrates creating a response with storage disabled using the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/delete_response.py b/examples/agenthub/openai/responses/delete_response.py index a41a607..d2d0553 100644 --- a/examples/agenthub/openai/responses/delete_response.py +++ b/examples/agenthub/openai/responses/delete_response.py @@ -3,11 +3,11 @@ """Demonstrates deleting a response from the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create a response first response = client.responses.create( diff --git a/examples/agenthub/openai/responses/file_input_with_file_id.py b/examples/agenthub/openai/responses/file_input_with_file_id.py index 3e8ae9e..47da9dd 100644 --- a/examples/agenthub/openai/responses/file_input_with_file_id.py +++ b/examples/agenthub/openai/responses/file_input_with_file_id.py @@ -3,11 +3,11 @@ """Demonstrates providing file input by file ID to the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Upload a file first with open("../files/sample_doc.pdf", "rb") as f: diff --git a/examples/agenthub/openai/responses/file_input_with_file_url.py b/examples/agenthub/openai/responses/file_input_with_file_url.py index 2df8274..7a00e79 100644 --- a/examples/agenthub/openai/responses/file_input_with_file_url.py +++ b/examples/agenthub/openai/responses/file_input_with_file_url.py @@ -3,11 +3,11 @@ """Demonstrates providing file input by URL to the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/get_response.py b/examples/agenthub/openai/responses/get_response.py index a3cc952..adde2a4 100644 --- a/examples/agenthub/openai/responses/get_response.py +++ b/examples/agenthub/openai/responses/get_response.py @@ -3,11 +3,11 @@ """Demonstrates retrieving a response from the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create a response first response = client.responses.create( diff --git a/examples/agenthub/openai/responses/image_input_with_base64.py b/examples/agenthub/openai/responses/image_input_with_base64.py index 17a258a..e9be2b0 100644 --- a/examples/agenthub/openai/responses/image_input_with_base64.py +++ b/examples/agenthub/openai/responses/image_input_with_base64.py @@ -6,7 +6,7 @@ import base64 from pathlib import Path -from examples import common +from examples.agenthub.openai import common def encode_image(image_path): @@ -15,7 +15,7 @@ def encode_image(image_path): def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # assuming the file "Cat.jpg" is in the same directory as this script image_file_path = Path(__file__).parent / "Cat.jpg" diff --git a/examples/agenthub/openai/responses/image_input_with_url.py b/examples/agenthub/openai/responses/image_input_with_url.py index b7bdb55..6207edc 100644 --- a/examples/agenthub/openai/responses/image_input_with_url.py +++ b/examples/agenthub/openai/responses/image_input_with_url.py @@ -3,11 +3,11 @@ """Demonstrates providing image input via URL to the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/reasoning.py b/examples/agenthub/openai/responses/reasoning.py index 45ff65b..7224283 100644 --- a/examples/agenthub/openai/responses/reasoning.py +++ b/examples/agenthub/openai/responses/reasoning.py @@ -3,11 +3,11 @@ """Demonstrates a reasoning-style Responses API request.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() prompt = """ Write a bash script that takes a matrix represented as a string with diff --git a/examples/agenthub/openai/responses/streaming_text_delta.py b/examples/agenthub/openai/responses/streaming_text_delta.py index 22372c2..5180be2 100644 --- a/examples/agenthub/openai/responses/streaming_text_delta.py +++ b/examples/agenthub/openai/responses/streaming_text_delta.py @@ -3,11 +3,11 @@ """Demonstrates streaming Responses API output and handling text deltas.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response_stream = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/structured_output.py b/examples/agenthub/openai/responses/structured_output.py index ab8ce9a..0633ad2 100644 --- a/examples/agenthub/openai/responses/structured_output.py +++ b/examples/agenthub/openai/responses/structured_output.py @@ -5,7 +5,7 @@ from pydantic import BaseModel -from examples import common +from examples.agenthub.openai import common class CalendarEvent(BaseModel): @@ -15,7 +15,7 @@ class CalendarEvent(BaseModel): def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.parse( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/responses/use_gpt_model.py b/examples/agenthub/openai/responses/use_gpt_model.py index 2a61aaf..fb76f1c 100644 --- a/examples/agenthub/openai/responses/use_gpt_model.py +++ b/examples/agenthub/openai/responses/use_gpt_model.py @@ -3,11 +3,11 @@ """Demonstrates using a GPT model with the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="openai.gpt-5.2", diff --git a/examples/agenthub/openai/responses/use_gptoss_model_dac.py b/examples/agenthub/openai/responses/use_gptoss_model_dac.py index c4111eb..ed8764b 100644 --- a/examples/agenthub/openai/responses/use_gptoss_model_dac.py +++ b/examples/agenthub/openai/responses/use_gptoss_model_dac.py @@ -3,11 +3,11 @@ """Demonstrates using a GPT OSS DAC model with the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="", diff --git a/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py b/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py index 2a4a68d..25400d1 100644 --- a/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py +++ b/examples/agenthub/openai/responses/use_gptoss_model_ondemand.py @@ -3,11 +3,11 @@ """Demonstrates using a GPT OSS on-demand model with the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="openai.gpt-oss-120b", diff --git a/examples/agenthub/openai/responses/use_grok_model.py b/examples/agenthub/openai/responses/use_grok_model.py index 067e40d..9d2cab4 100644 --- a/examples/agenthub/openai/responses/use_grok_model.py +++ b/examples/agenthub/openai/responses/use_grok_model.py @@ -3,11 +3,11 @@ """Demonstrates using a Grok model with the Responses API.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/tools/code_interpreter.py b/examples/agenthub/openai/tools/code_interpreter.py index 164f11f..e749d28 100644 --- a/examples/agenthub/openai/tools/code_interpreter.py +++ b/examples/agenthub/openai/tools/code_interpreter.py @@ -3,11 +3,11 @@ """Demonstrates the code_interpreter tool in AgentHub.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/tools/file_search.py b/examples/agenthub/openai/tools/file_search.py index 9ae5176..573a723 100644 --- a/examples/agenthub/openai/tools/file_search.py +++ b/examples/agenthub/openai/tools/file_search.py @@ -3,13 +3,13 @@ """Demonstrates the file_search tool in AgentHub.""" -from examples import common +from examples.agenthub.openai import common VECTOR_STORE_ID = "<>" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/tools/function_calling.py b/examples/agenthub/openai/tools/function_calling.py index 453a1f9..22d50d9 100644 --- a/examples/agenthub/openai/tools/function_calling.py +++ b/examples/agenthub/openai/tools/function_calling.py @@ -8,7 +8,7 @@ from openai.types.responses import ResponseFunctionToolCall from openai.types.responses.response_input_param import FunctionCallOutput -from examples import common +from examples.agenthub.openai import common model = "xai.grok-4-1-fast-reasoning" @@ -25,7 +25,7 @@ def get_current_weather(location: str) -> dict: def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Define function tool schema function_tools = [ diff --git a/examples/agenthub/openai/tools/image_generation.py b/examples/agenthub/openai/tools/image_generation.py index 29bb1bc..49fa1c3 100644 --- a/examples/agenthub/openai/tools/image_generation.py +++ b/examples/agenthub/openai/tools/image_generation.py @@ -5,11 +5,11 @@ import base64 -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="openai.gpt-5.2", diff --git a/examples/agenthub/openai/tools/multiple_tools.py b/examples/agenthub/openai/tools/multiple_tools.py index d8f4b81..869b459 100644 --- a/examples/agenthub/openai/tools/multiple_tools.py +++ b/examples/agenthub/openai/tools/multiple_tools.py @@ -3,11 +3,11 @@ """Demonstrates using multiple tools in a single request.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response_stream = client.responses.create( model="openai.gpt-5.1", diff --git a/examples/agenthub/openai/tools/remote_mcp.py b/examples/agenthub/openai/tools/remote_mcp.py index 2bbc3d6..0b5bd66 100644 --- a/examples/agenthub/openai/tools/remote_mcp.py +++ b/examples/agenthub/openai/tools/remote_mcp.py @@ -3,11 +3,11 @@ """Demonstrates calling a remote MCP tool.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response_stream = client.responses.create( model="xai.grok-4-1-fast-reasoning", diff --git a/examples/agenthub/openai/tools/remote_mcp_approval_flow.py b/examples/agenthub/openai/tools/remote_mcp_approval_flow.py index b4c3be8..2d2a5d8 100644 --- a/examples/agenthub/openai/tools/remote_mcp_approval_flow.py +++ b/examples/agenthub/openai/tools/remote_mcp_approval_flow.py @@ -3,11 +3,11 @@ """Demonstrates an approval flow for remote MCP tools.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # First API request - Ask the model to call the MCP server, # and requires your approval to execute the tool call diff --git a/examples/agenthub/openai/tools/web_search.py b/examples/agenthub/openai/tools/web_search.py index a79283b..dcd2fb8 100644 --- a/examples/agenthub/openai/tools/web_search.py +++ b/examples/agenthub/openai/tools/web_search.py @@ -3,11 +3,11 @@ """Demonstrates the web_search tool in AgentHub.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response = client.responses.create( model="openai.gpt-5.1", diff --git a/examples/agenthub/openai/tools/web_search_streaming.py b/examples/agenthub/openai/tools/web_search_streaming.py index 2214432..8234a08 100644 --- a/examples/agenthub/openai/tools/web_search_streaming.py +++ b/examples/agenthub/openai/tools/web_search_streaming.py @@ -3,11 +3,11 @@ """Demonstrates streaming results from the web_search tool.""" -from examples import common +from examples.agenthub.openai import common def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() response_stream = client.responses.create( model="openai.gpt-5.1", diff --git a/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py b/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py index bd50c32..976dba4 100644 --- a/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py +++ b/examples/agenthub/openai/vector_stores/vector_store_file_batches_crud.py @@ -3,13 +3,13 @@ """Demonstrates CRUD operations for vector store file batches.""" -from examples import common +from examples.agenthub.openai import common VECTOR_STORE_ID = "<>" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() with open("../files/sample_doc.pdf", "rb") as f1, open("../files/sample_doc.pdf", "rb") as f2: file_1 = client.files.create( file=f1, diff --git a/examples/agenthub/openai/vector_stores/vector_store_files_crud.py b/examples/agenthub/openai/vector_stores/vector_store_files_crud.py index 67d81ff..0a39177 100644 --- a/examples/agenthub/openai/vector_stores/vector_store_files_crud.py +++ b/examples/agenthub/openai/vector_stores/vector_store_files_crud.py @@ -3,13 +3,13 @@ """Demonstrates CRUD operations for vector store files.""" -from examples import common +from examples.agenthub.openai import common VECTOR_STORE_ID = "<>" def main(): - client = common.build_openai_agenthub_client() + client = common.build_agenthub_client() # Create the file with open("../files/sample_doc.pdf", "rb") as f: file = client.files.create( diff --git a/examples/agenthub/openai/vector_stores/vector_stores_crud.py b/examples/agenthub/openai/vector_stores/vector_stores_crud.py index cf36fdf..9f1343e 100644 --- a/examples/agenthub/openai/vector_stores/vector_stores_crud.py +++ b/examples/agenthub/openai/vector_stores/vector_stores_crud.py @@ -3,11 +3,11 @@ """Vector Stores API examples - create, list, retrieve, update, search, and delete.""" -from examples import common +from examples.agenthub.openai import common def main(): - cp_client = common.build_openai_agenthub_cp_client() + cp_client = common.build_agenthub_cp_client() # Create a vector store vector_store = cp_client.vector_stores.create( name="OCI Support FAQ Vector Store", diff --git a/examples/agenthub/openai/vector_stores/vector_stores_search.py b/examples/agenthub/openai/vector_stores/vector_stores_search.py index d46871e..19f548a 100644 --- a/examples/agenthub/openai/vector_stores/vector_stores_search.py +++ b/examples/agenthub/openai/vector_stores/vector_stores_search.py @@ -5,11 +5,11 @@ import time -from examples import common +from examples.agenthub.openai import common def main(): - dp_client = common.build_openai_agenthub_client() + dp_client = common.build_agenthub_client() with open("../files/sample_doc.pdf", "rb") as f: # Upload a file using the Files API file = dp_client.files.create( @@ -19,7 +19,7 @@ def main(): print(f"Uploaded file:{file.id}, waiting for it to be processed") # dp_client.files.wait_for_processing(file.id) - cp_client = common.build_openai_agenthub_cp_client() + cp_client = common.build_agenthub_cp_client() # Create a vector store vector_store = cp_client.vector_stores.create( name="OCI Support FAQ Vector Store", diff --git a/examples/common.py b/examples/common.py deleted file mode 100644 index 377192b..0000000 --- a/examples/common.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) 2026 Oracle and/or its affiliates. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ - -"""Demonstrates the Common example.""" - -import os - -import httpx -from oci_genai_support.openai.oci_openai import OPC_COMPARTMENT_ID_HEADER -from openai import AsyncOpenAI, OpenAI - -from oci_genai_auth import OciSessionAuth - -""" -Common clients used by examples. - -- AgentHub (non-pass-through) provide build_openai_agenthub_client (DP) - and build_openai_agenthub_cp_client (CP). -- Partner (pass-through) examples use `build_openai_client` helpers. -""" - -# Shared defaults. -PROFILE_NAME = "DEFAULT" -COMPARTMENT_ID = "<>" -PROJECT_OCID = "<>" - -REGION = "us-chicago-1" - -# Partner (pass-through) base URL. -PARTNER_OPENAI_BASE_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/v1" - -# AgentHub (non-pass-through) base URLs. -AGENTHUB_OPENAI_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/openai/v1" -AGENTHUB_OPENAI_CP_URL = f"https://generativeai.{REGION}.oci.oraclecloud.com/20231130/openai/v1" - - -def build_openai_agenthub_client(): - return OpenAI( - base_url=AGENTHUB_OPENAI_URL, - api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), - project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), - http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), - ) - - -def build_openai_agenthub_cp_client(): - return OpenAI( - base_url=AGENTHUB_OPENAI_CP_URL, - api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), - http_client=httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), - project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), - default_headers={OPC_COMPARTMENT_ID_HEADER: COMPARTMENT_ID}, - ) - - -def build_openai_agenthub_async_client(): - return AsyncOpenAI( - base_url=AGENTHUB_OPENAI_URL, - api_key=os.getenv("OCI_GENAI_API_KEY", "not-used"), - project=os.getenv("OCI_GENAI_PROJECT_ID", PROJECT_OCID), - http_client=httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), - ) - - -def build_openai_pt_client() -> OpenAI: - client_kwargs = { - "api_key": "not-used", - "base_url": PARTNER_OPENAI_BASE_URL, - "http_client": httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), - } - if COMPARTMENT_ID: - client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} - return OpenAI(**client_kwargs) - - -def build_openai_pt_async_client() -> AsyncOpenAI: - client_kwargs = { - "api_key": "not-used", - "base_url": PARTNER_OPENAI_BASE_URL, - "http_client": httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), - } - if COMPARTMENT_ID: - client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} - return AsyncOpenAI(**client_kwargs) diff --git a/examples/partner/openai/basic_chat_completion.py b/examples/partner/openai/basic_chat_completion.py index 0ce7b6f..da0e4a4 100644 --- a/examples/partner/openai/basic_chat_completion.py +++ b/examples/partner/openai/basic_chat_completion.py @@ -5,7 +5,7 @@ from rich import print -from examples import common +from examples.partner.openai import common MODEL = "openai.gpt-4.1" diff --git a/examples/partner/openai/common.py b/examples/partner/openai/common.py new file mode 100644 index 0000000..874b325 --- /dev/null +++ b/examples/partner/openai/common.py @@ -0,0 +1,39 @@ +# Copyright (c) 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + +"""Partner (pass-through) example clients and configuration.""" + +from __future__ import annotations + +import httpx +from openai import AsyncOpenAI, OpenAI + +from oci_genai_auth import OciSessionAuth + +PROFILE_NAME = "DEFAULT" +COMPARTMENT_ID = "<>" +REGION = "us-chicago-1" + +PARTNER_OPENAI_BASE_URL = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com/v1" + + +def build_openai_client() -> OpenAI: + client_kwargs = { + "api_key": "not-used", + "base_url": PARTNER_OPENAI_BASE_URL, + "http_client": httpx.Client(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + } + if COMPARTMENT_ID: + client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} + return OpenAI(**client_kwargs) + + +def build_openai_async_client() -> AsyncOpenAI: + client_kwargs = { + "api_key": "not-used", + "base_url": PARTNER_OPENAI_BASE_URL, + "http_client": httpx.AsyncClient(auth=OciSessionAuth(profile_name=PROFILE_NAME)), + } + if COMPARTMENT_ID: + client_kwargs["default_headers"] = {"opc-compartment-id": COMPARTMENT_ID} + return AsyncOpenAI(**client_kwargs) diff --git a/examples/partner/openai/quickstart_openai_chat_completions.py b/examples/partner/openai/quickstart_openai_chat_completions.py index 8335bc6..8da3e94 100755 --- a/examples/partner/openai/quickstart_openai_chat_completions.py +++ b/examples/partner/openai/quickstart_openai_chat_completions.py @@ -5,7 +5,7 @@ import logging -from examples import common +from examples.partner.openai import common logging.basicConfig(level=logging.DEBUG) diff --git a/examples/partner/openai/streaming_chat_completion.py b/examples/partner/openai/streaming_chat_completion.py index ab2ee87..8a3065b 100644 --- a/examples/partner/openai/streaming_chat_completion.py +++ b/examples/partner/openai/streaming_chat_completion.py @@ -3,7 +3,7 @@ """Demonstrates streaming chat completion responses for the Partner (pass-through) endpoint.""" -from examples import common +from examples.partner.openai import common MODEL = "openai.gpt-4.1" diff --git a/examples/partner/openai/tool_call_chat_completion.py b/examples/partner/openai/tool_call_chat_completion.py index 3456093..fab4cdb 100644 --- a/examples/partner/openai/tool_call_chat_completion.py +++ b/examples/partner/openai/tool_call_chat_completion.py @@ -6,7 +6,7 @@ import json from typing import Dict -from examples import common +from examples.partner.openai import common MODEL = "openai.gpt-4.1"