From 11df7cdce5021d977bf10c286299b353e9d69bbe Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 19 Feb 2026 16:03:57 -0800 Subject: [PATCH 01/16] Initial implementation of OCI GenAI Unified Java SDK Multi-module Maven project (core + provider modules + BOM) for integrating OpenAI and Anthropic with OCI authentication and routing. Modules: - oci-genai-bom: Version management BOM - oci-genai-core: OCI IAM auth, request signing, header injection, endpoint resolution - oci-genai-openai: Wraps openai-java SDK with OCI signing via custom HttpClient - oci-genai-anthropic: Wraps anthropic-sdk-java with OCI signing via custom HttpClient Supports all 4 OCI IAM auth types: oci_config, security_token, instance_principal, resource_principal. Includes sync and async client builders for both providers, and unit tests for core module. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 22 ++ README.md | 319 ++++++++++++++++-- oci-genai-anthropic/pom.xml | 42 +++ .../genai/anthropic/AsyncOciAnthropic.java | 105 ++++++ .../oracle/genai/anthropic/OciAnthropic.java | 194 +++++++++++ .../genai/anthropic/OciSigningHttpClient.java | 186 ++++++++++ oci-genai-bom/pom.xml | 105 ++++++ oci-genai-core/pom.xml | 54 +++ .../genai/core/OciHttpClientFactory.java | 84 +++++ .../genai/core/auth/OciAuthException.java | 21 ++ .../core/auth/OciAuthProviderFactory.java | 114 +++++++ .../core/endpoint/OciEndpointResolver.java | 84 +++++ .../interceptor/OciHeaderInterceptor.java | 65 ++++ .../interceptor/OciSigningInterceptor.java | 108 ++++++ .../core/auth/OciAuthProviderFactoryTest.java | 47 +++ .../endpoint/OciEndpointResolverTest.java | 89 +++++ .../interceptor/OciHeaderInterceptorTest.java | 89 +++++ oci-genai-openai/pom.xml | 42 +++ .../oracle/genai/openai/AsyncOciOpenAI.java | 123 +++++++ .../com/oracle/genai/openai/OciOpenAI.java | 237 +++++++++++++ .../genai/openai/OciSigningHttpClient.java | 184 ++++++++++ pom.xml | 139 ++++++++ 22 files changed, 2430 insertions(+), 23 deletions(-) create mode 100644 .gitignore create mode 100644 oci-genai-anthropic/pom.xml create mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java create mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java create mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java create mode 100644 oci-genai-bom/pom.xml create mode 100644 oci-genai-core/pom.xml create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java create mode 100644 oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java create mode 100644 oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java create mode 100644 oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java create mode 100644 oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java create mode 100644 oci-genai-openai/pom.xml create mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java create mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java create mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f74b63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Build output +target/ + +# IDE files +.idea/ +*.iml +.project +.classpath +.settings/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Maven +dependency-reduced-pom.xml + +# Logs +*.log diff --git a/README.md b/README.md index 73e8102..e50aa97 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,317 @@ -*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.* +# OCI GenAI Unified Java SDK -# Project name +Unified Java SDK family for integrating third-party Generative AI providers (OpenAI, Anthropic) with Oracle Cloud Infrastructure authentication and routing. -*Describe your project's features, functionality and target audience* +## Table of Contents + +- [Architecture](#architecture) +- [Installation](#installation) +- [Quick Start](#quick-start) + - [OpenAI](#openai) + - [Anthropic](#anthropic) +- [Authentication](#authentication) +- [Client Configuration](#client-configuration) +- [Async Clients](#async-clients) +- [Base URL and Endpoint Overrides](#base-url-and-endpoint-overrides) +- [Error Handling](#error-handling) +- [Cleanup](#cleanup) +- [Module Reference](#module-reference) +- [Building from Source](#building-from-source) +- [License](#license) + +## Architecture + +This SDK follows the **core + provider modules + BOM** pattern used by AWS SDK v2, Azure SDK for Java, Google Cloud Java, and OCI's own existing SDK. Users import only the provider modules they need — no forced dependency bloat. + +``` +oci-genai-bom Version management only (BOM) +oci-genai-core OCI IAM auth, request signing, header injection, endpoint resolution +oci-genai-openai Wraps openai-java SDK with OCI signing +oci-genai-anthropic Wraps anthropic-sdk-java with OCI signing +``` + +All modules share `oci-genai-core` for OCI authentication — signing logic is implemented once and applied consistently across all providers. ## 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* +The SDK requires **Java 17+**. Add the BOM and the provider modules you need: -## Documentation +```xml + + + + com.oracle.genai + oci-genai-bom + 0.1.0-SNAPSHOT + pom + import + + + -*Developer-oriented documentation can be published on GitHub, but all product documentation must be published on * + + + + com.oracle.genai + oci-genai-openai + -## Examples + + + com.oracle.genai + oci-genai-anthropic + + +``` -*Describe any included examples or provide a link to a demo/tutorial* +Import only the providers you use. Each module brings in only its own dependencies. -## Help +## Quick Start -*Inform users on where to get help or how to receive official support from Oracle (if applicable)* +### OpenAI -## Contributing +```java +import com.openai.client.OpenAIClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.oracle.genai.openai.OciOpenAI; -*If your project has specific contribution requirements, update the CONTRIBUTING.md file to ensure those requirements are clearly explained* +public class OpenAIQuickStart { + public static void main(String[] args) { + OpenAIClient client = OciOpenAI.builder() + .compartmentId("") + .authType("security_token") + .profile("DEFAULT") + .region("us-chicago-1") + .build(); -This project welcomes contributions from the community. Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md) + try { + Response response = client.responses().create(ResponseCreateParams.builder() + .model("openai.gpt-4o") + .store(false) + .input("Write a short poem about cloud computing.") + .build()); -## Security + System.out.println(response.output()); + } finally { + client.close(); + } + } +} +``` -Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process +### Anthropic -## License +```java +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.Model; +import com.oracle.genai.anthropic.OciAnthropic; + +public class AnthropicQuickStart { + public static void main(String[] args) { + AnthropicClient client = OciAnthropic.builder() + .compartmentId("") + .authType("security_token") + .profile("DEFAULT") + .region("us-chicago-1") + .build(); + + try { + Message message = client.messages().create(MessageCreateParams.builder() + .model(Model.CLAUDE_SONNET_4_20250514) + .addUserMessage("Hello from OCI!") + .maxTokens(1024) + .build()); + + System.out.println(message.content()); + } finally { + client.close(); + } + } +} +``` + +## Authentication + +Both `OciOpenAI` and `OciAnthropic` support all four OCI IAM authentication types through the `authType` parameter: + +| Auth Type | Use Case | +|-----------|----------| +| `oci_config` | Local development with API key in `~/.oci/config` | +| `security_token` | Local development with OCI CLI session token | +| `instance_principal` | OCI Compute instances with dynamic group policies | +| `resource_principal` | OCI Functions, Container Instances | + +```java +// 1) User principal (API key) +OpenAIClient client = OciOpenAI.builder() + .authType("oci_config") + .profile("DEFAULT") + .compartmentId("") + .region("us-chicago-1") + .build(); + +// 2) Session token (local dev) +OpenAIClient client = OciOpenAI.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId("") + .region("us-chicago-1") + .build(); + +// 3) Instance principal (OCI Compute) +OpenAIClient client = OciOpenAI.builder() + .authType("instance_principal") + .compartmentId("") + .region("us-chicago-1") + .build(); + +// 4) Resource principal (OCI Functions) +OpenAIClient client = OciOpenAI.builder() + .authType("resource_principal") + .compartmentId("") + .region("us-chicago-1") + .build(); + +// 5) Custom auth provider +BasicAuthenticationDetailsProvider authProvider = /* your provider */; +OpenAIClient client = OciOpenAI.builder() + .authProvider(authProvider) + .compartmentId("") + .region("us-chicago-1") + .build(); +``` + +The same `authType` and `authProvider` parameters work identically for `OciAnthropic`. + +## Client Configuration + +| Parameter | Description | Required | +|-----------|-------------|----------| +| `compartmentId` | OCI compartment OCID | Yes (for GenAI endpoints) | +| `authType` or `authProvider` | Authentication mechanism | Yes | +| `region` | OCI region code (e.g., `us-chicago-1`) | Yes (unless `baseUrl` or `serviceEndpoint` is set) | +| `baseUrl` | Fully qualified endpoint override | No | +| `serviceEndpoint` | Service endpoint without API path | No | +| `conversationStoreId` | Conversation Store OCID (OpenAI only) | No | +| `timeout` | Request timeout (default: 2 minutes) | No | +| `logRequestsAndResponses` | Debug logging of HTTP bodies | No | +| `profile` | OCI config profile name (default: `DEFAULT`) | No | + +## Async Clients + +Both providers include async client builders that return `CompletableFuture`-based clients: + +```java +import com.oracle.genai.openai.AsyncOciOpenAI; +import com.oracle.genai.anthropic.AsyncOciAnthropic; -*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* +// Async OpenAI +OpenAIClientAsync openaiAsync = AsyncOciOpenAI.builder() + .compartmentId("") + .authType("security_token") + .region("us-chicago-1") + .build(); -Copyright (c) 2026 Oracle and/or its affiliates. +openaiAsync.responses().create(params) + .thenAccept(response -> System.out.println(response.output())); + +// Async Anthropic +AnthropicClientAsync anthropicAsync = AsyncOciAnthropic.builder() + .compartmentId("") + .authType("security_token") + .region("us-chicago-1") + .build(); + +anthropicAsync.messages().create(params) + .thenAccept(message -> System.out.println(message.content())); +``` + +## Base URL and Endpoint Overrides + +Endpoint resolution priority (highest to lowest): `baseUrl` > `serviceEndpoint` > `region`. + +```java +// From region (most common) +OciOpenAI.builder() + .region("us-chicago-1") + // resolves to: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1 + ... + +// From service endpoint (SDK appends API path) +OciOpenAI.builder() + .serviceEndpoint("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com") + // resolves to: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1 + ... + +// From explicit base URL (used as-is) +OciOpenAI.builder() + .baseUrl("https://custom-endpoint.example.com/v1") + ... +``` + +## Error Handling + +The underlying provider SDK exceptions still apply. Catch provider-specific exceptions for error handling: + +```java +// OpenAI +try { + Response response = client.responses().create(params); +} catch (com.openai.errors.NotFoundException | com.openai.errors.UnauthorizedException e) { + System.err.println("Error: " + e.getMessage()); +} + +// Anthropic +try { + Message message = client.messages().create(params); +} catch (com.anthropic.errors.NotFoundException | com.anthropic.errors.UnauthorizedException e) { + System.err.println("Error: " + e.getMessage()); +} +``` + +## Cleanup + +Both client types implement `AutoCloseable`. Close them when finished to release HTTP resources: + +```java +try (OpenAIClient client = OciOpenAI.builder() + .compartmentId("") + .authType("security_token") + .region("us-chicago-1") + .build()) { + // use client +} +``` + +## Module Reference + +| Module | Artifact | Responsibility | +|--------|----------|----------------| +| `oci-genai-bom` | `com.oracle.genai:oci-genai-bom` | Pins all module and transitive dependency versions | +| `oci-genai-core` | `com.oracle.genai:oci-genai-core` | OCI IAM auth providers, per-request signing, header injection, endpoint resolution | +| `oci-genai-openai` | `com.oracle.genai:oci-genai-openai` | Wraps `openai-java` with OCI signing via custom `HttpClient` | +| `oci-genai-anthropic` | `com.oracle.genai:oci-genai-anthropic` | Wraps `anthropic-sdk-java` with OCI signing via custom `HttpClient` | + +## Building from Source + +Requires Java 17+ and Maven 3.8+. + +```bash +# Compile all modules +mvn clean compile + +# Run tests +mvn test + +# Install to local Maven repository +mvn install -DskipTests +``` + +## License -*Replace this statement if your project is not licensed under the UPL* +Copyright (c) 2025 Oracle and/or its affiliates. -Released under the Universal Permissive License v1.0 as shown at -. +Released under the [Universal Permissive License v1.0](https://oss.oracle.com/licenses/upl/). diff --git a/oci-genai-anthropic/pom.xml b/oci-genai-anthropic/pom.xml new file mode 100644 index 0000000..5aa793e --- /dev/null +++ b/oci-genai-anthropic/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + + com.oracle.genai + oci-genai-parent + 0.1.0-SNAPSHOT + + + oci-genai-anthropic + jar + + OCI GenAI SDK :: Anthropic + + Anthropic provider module for the OCI GenAI SDK. + Wraps the official anthropic-sdk-java with OCI IAM authentication + via OkHttp interceptor. Omits X-Api-Key when calling OCI endpoints. + + + + + + com.oracle.genai + oci-genai-core + + + + + com.anthropic + anthropic-java + + + diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java new file mode 100644 index 0000000..fda6a25 --- /dev/null +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import com.anthropic.client.AnthropicClientAsync; +import com.anthropic.client.AnthropicClientAsyncImpl; +import com.anthropic.core.ClientOptions; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.genai.core.OciHttpClientFactory; +import com.oracle.genai.core.auth.OciAuthProviderFactory; +import com.oracle.genai.core.endpoint.OciEndpointResolver; + +import java.time.Duration; + +/** + * Async OCI-authenticated Anthropic client builder. + * + *

Creates an {@link AnthropicClientAsync} that routes requests through OCI + * Generative AI endpoints with OCI IAM request signing. + * + *

Quick Start

+ *
{@code
+ * AnthropicClientAsync client = AsyncOciAnthropic.builder()
+ *         .compartmentId("")
+ *         .authType("security_token")
+ *         .region("us-chicago-1")
+ *         .build();
+ *
+ * client.messages().create(MessageCreateParams.builder()
+ *         .model("anthropic.claude-3-sonnet")
+ *         .addUserMessage("Hello from OCI!")
+ *         .maxTokens(1024)
+ *         .build())
+ *     .thenAccept(message -> System.out.println(message.content()));
+ * }
+ */ +public final class AsyncOciAnthropic { + + private AsyncOciAnthropic() { + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String authType; + private String profile; + private BasicAuthenticationDetailsProvider authProvider; + private String compartmentId; + private String region; + private String serviceEndpoint; + private String baseUrl; + private Duration timeout; + private boolean logRequestsAndResponses; + + private Builder() { + } + + public Builder authType(String authType) { this.authType = authType; return this; } + public Builder profile(String profile) { this.profile = profile; return this; } + public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } + public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } + public Builder region(String region) { this.region = region; return this; } + public Builder serviceEndpoint(String serviceEndpoint) { this.serviceEndpoint = serviceEndpoint; return this; } + public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } + public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } + public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } + + public AnthropicClientAsync build() { + BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); + + String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( + region, serviceEndpoint, baseUrl); + + if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { + throw new IllegalArgumentException( + "compartmentId is required to access the OCI Generative AI Service."); + } + + okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( + resolvedAuth, compartmentId, null, timeout, logRequestsAndResponses); + + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(resolvedBaseUrl) + .build(); + + return new AnthropicClientAsyncImpl(clientOptions); + } + + private BasicAuthenticationDetailsProvider resolveAuthProvider() { + if (authProvider != null) return authProvider; + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException("Either authType or authProvider must be provided."); + } + return OciAuthProviderFactory.create(authType, profile); + } + } +} diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java new file mode 100644 index 0000000..174b5fe --- /dev/null +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.AnthropicClientImpl; +import com.anthropic.core.ClientOptions; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.genai.core.OciHttpClientFactory; +import com.oracle.genai.core.auth.OciAuthProviderFactory; +import com.oracle.genai.core.endpoint.OciEndpointResolver; + +import java.time.Duration; + +/** + * OCI-authenticated Anthropic client builder. + * + *

Creates an {@link AnthropicClient} that routes requests through OCI Generative AI + * endpoints with OCI IAM request signing. The underlying Anthropic Java SDK is used + * for all API operations — users get the full Anthropic API surface (messages, + * streaming, tool use) with OCI auth handled transparently. + * + *

Quick Start

+ *
{@code
+ * AnthropicClient client = OciAnthropic.builder()
+ *         .compartmentId("")
+ *         .authType("security_token")
+ *         .region("us-chicago-1")
+ *         .build();
+ *
+ * Message message = client.messages().create(MessageCreateParams.builder()
+ *         .model("anthropic.claude-3-sonnet")
+ *         .addUserMessage("Hello from OCI!")
+ *         .maxTokens(1024)
+ *         .build());
+ * }
+ * + *

Authentication

+ *

Supports all OCI IAM auth types via {@code authType}: + *

    + *
  • {@code oci_config} — user principal from {@code ~/.oci/config}
  • + *
  • {@code security_token} — session token from OCI CLI
  • + *
  • {@code instance_principal} — OCI Compute instances
  • + *
  • {@code resource_principal} — OCI Functions, Container Instances
  • + *
+ *

Alternatively, pass a pre-built {@link BasicAuthenticationDetailsProvider} + * via {@code authProvider()}. + */ +public final class OciAnthropic { + + private OciAnthropic() { + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String authType; + private String profile; + private BasicAuthenticationDetailsProvider authProvider; + private String compartmentId; + private String region; + private String serviceEndpoint; + private String baseUrl; + private Duration timeout; + private boolean logRequestsAndResponses; + + private Builder() { + } + + /** + * Sets the OCI authentication type. + * One of: {@code oci_config}, {@code security_token}, + * {@code instance_principal}, {@code resource_principal}. + */ + public Builder authType(String authType) { + this.authType = authType; + return this; + } + + /** + * Sets the OCI config profile name. Used with {@code oci_config} and + * {@code security_token} auth types. Defaults to {@code "DEFAULT"}. + */ + public Builder profile(String profile) { + this.profile = profile; + return this; + } + + /** + * Sets a pre-built OCI authentication provider. + * When set, {@code authType} and {@code profile} are ignored. + */ + public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { + this.authProvider = authProvider; + return this; + } + + /** + * Sets the OCI compartment OCID. Required for OCI Generative AI endpoints. + */ + public Builder compartmentId(String compartmentId) { + this.compartmentId = compartmentId; + return this; + } + + /** + * Sets the OCI region code (e.g., {@code "us-chicago-1"}). + */ + public Builder region(String region) { + this.region = region; + return this; + } + + /** + * Sets the OCI service endpoint (without API path). + * The Anthropic API path is appended automatically. + */ + public Builder serviceEndpoint(String serviceEndpoint) { + this.serviceEndpoint = serviceEndpoint; + return this; + } + + /** + * Sets the fully qualified base URL. Used as-is without modification. + */ + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** Sets the request timeout. Defaults to 2 minutes. */ + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** Enables debug logging of request/response bodies. */ + public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { + this.logRequestsAndResponses = logRequestsAndResponses; + return this; + } + + /** + * Builds the OCI-authenticated Anthropic client. + * + * @return a configured {@link AnthropicClient} + * @throws IllegalArgumentException if required parameters are missing + */ + public AnthropicClient build() { + BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); + + String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( + region, serviceEndpoint, baseUrl); + + if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { + throw new IllegalArgumentException( + "compartmentId is required to access the OCI Generative AI Service."); + } + + // Create OCI-signed OkHttpClient from core + okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( + resolvedAuth, compartmentId, null, timeout, logRequestsAndResponses); + + // Create a signing HTTP client that implements the Anthropic SDK's HttpClient interface. + // This strips X-Api-Key headers and delegates to the OCI-signed OkHttpClient. + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + + // Build ClientOptions with our signing HTTP client and base URL. + // No API key is needed — OCI signing replaces Anthropic API key authentication. + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(resolvedBaseUrl) + .build(); + + return new AnthropicClientImpl(clientOptions); + } + + private BasicAuthenticationDetailsProvider resolveAuthProvider() { + if (authProvider != null) { + return authProvider; + } + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException( + "Either authType or authProvider must be provided."); + } + return OciAuthProviderFactory.create(authType, profile); + } + } +} diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java new file mode 100644 index 0000000..0e6a39b --- /dev/null +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import com.anthropic.core.RequestOptions; +import com.anthropic.core.http.HttpClient; +import com.anthropic.core.http.HttpRequest; +import com.anthropic.core.http.HttpRequestBody; +import com.anthropic.core.http.HttpResponse; +import com.anthropic.core.http.Headers; +import okhttp3.*; +import okio.BufferedSink; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; + +/** + * An implementation of the Anthropic SDK's {@link HttpClient} interface backed by + * an OCI-signed {@link okhttp3.OkHttpClient}. + * + *

This bridges the Anthropic SDK's HTTP abstraction with OCI request signing. + * The underlying OkHttpClient has {@code OciSigningInterceptor} and + * {@code OciHeaderInterceptor} already configured, so every request is + * automatically signed with OCI IAM credentials. + */ +class OciSigningHttpClient implements HttpClient { + + private static final Logger LOG = LoggerFactory.getLogger(OciSigningHttpClient.class); + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); + + private final okhttp3.OkHttpClient okHttpClient; + + OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + try { + Response okResponse = okHttpClient.newCall(okRequest).execute(); + return new OkHttpResponseAdapter(okResponse); + } catch (IOException e) { + throw new RuntimeException("OCI request failed: " + request.url(), e); + } + } + + @Override + public CompletableFuture executeAsync( + HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + CompletableFuture future = new CompletableFuture<>(); + + okHttpClient.newCall(okRequest).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally( + new RuntimeException("OCI async request failed: " + request.url(), e)); + } + + @Override + public void onResponse(Call call, Response response) { + future.complete(new OkHttpResponseAdapter(response)); + } + }); + + return future; + } + + @Override + public void close() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + + private Request toOkHttpRequest(HttpRequest request) { + String url = request.url(); + HttpUrl parsedUrl = HttpUrl.parse(url); + if (parsedUrl == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + + HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); + + // Add query params + var queryParams = request.queryParams(); + for (String key : queryParams.keys()) { + for (String value : queryParams.values(key)) { + urlBuilder.addQueryParameter(key, value); + } + } + + // Build headers (strip X-Api-Key since OCI signing replaces it) + okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); + var headers = request.headers(); + for (String name : headers.names()) { + // Omit API key / auth headers — OCI signing handles authentication + if ("x-api-key".equalsIgnoreCase(name) || "authorization".equalsIgnoreCase(name)) { + continue; + } + for (String value : headers.values(name)) { + headersBuilder.add(name, value); + } + } + + // Build request body + RequestBody body = null; + HttpRequestBody requestBody = request.body(); + if (requestBody != null) { + body = new RequestBody() { + @Override + public MediaType contentType() { + String ct = requestBody.contentType(); + return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return requestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (OutputStream os = sink.outputStream()) { + requestBody.writeTo(os); + } + } + }; + } + + String method = request.method().name(); + return new Request.Builder() + .url(urlBuilder.build()) + .headers(headersBuilder.build()) + .method(method, body) + .build(); + } + + /** + * Adapts an OkHttp {@link Response} to the Anthropic SDK's {@link HttpResponse} interface. + */ + private static class OkHttpResponseAdapter implements HttpResponse { + + private final Response response; + private final Headers headers; + + OkHttpResponseAdapter(Response response) { + this.response = response; + Headers.Builder builder = Headers.builder(); + for (String name : response.headers().names()) { + for (String value : response.headers(name)) { + builder.put(name, value); + } + } + this.headers = builder.build(); + } + + @Override + public int statusCode() { + return response.code(); + } + + @Override + public Headers headers() { + return headers; + } + + @Override + public InputStream body() { + ResponseBody responseBody = response.body(); + return responseBody != null ? responseBody.byteStream() : InputStream.nullInputStream(); + } + + @Override + public void close() { + response.close(); + } + } +} diff --git a/oci-genai-bom/pom.xml b/oci-genai-bom/pom.xml new file mode 100644 index 0000000..356f8db --- /dev/null +++ b/oci-genai-bom/pom.xml @@ -0,0 +1,105 @@ + + + + 4.0.0 + + com.oracle.genai + oci-genai-bom + 0.1.0-SNAPSHOT + pom + + OCI GenAI SDK :: BOM + + Bill of Materials (BOM) for the OCI GenAI SDK family. + Import this POM to pin all first-party module versions and key transitive + dependency versions, preventing diamond dependency conflicts. + + + + + 0.1.0-SNAPSHOT + + + 3.57.2 + + + 0.40.0 + 2.12.0 + + + 4.12.0 + 2.18.2 + 2.0.16 + + + + + + + com.oracle.genai + oci-genai-core + ${oci-genai.version} + + + com.oracle.genai + oci-genai-openai + ${oci-genai.version} + + + com.oracle.genai + oci-genai-anthropic + ${oci-genai.version} + + + + + com.oracle.oci.sdk + oci-java-sdk-common + ${oci-sdk.version} + + + + + com.openai + openai-java + ${openai-java.version} + + + com.anthropic + anthropic-java + ${anthropic-java.version} + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp.version} + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + diff --git a/oci-genai-core/pom.xml b/oci-genai-core/pom.xml new file mode 100644 index 0000000..bc9e941 --- /dev/null +++ b/oci-genai-core/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + + com.oracle.genai + oci-genai-parent + 0.1.0-SNAPSHOT + + + oci-genai-core + jar + + OCI GenAI SDK :: Core + + Shared foundation for the OCI GenAI SDK family. + Provides OCI IAM authentication providers, per-request signing interceptor, + compartment header injection, endpoint resolution, and retry policies. + + + + + + com.oracle.oci.sdk + oci-java-sdk-common + + + + + com.squareup.okhttp3 + okhttp + + + + + com.squareup.okhttp3 + logging-interceptor + + + + + org.slf4j + slf4j-api + + + diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java b/oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java new file mode 100644 index 0000000..3d9324d --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.genai.core.interceptor.OciHeaderInterceptor; +import com.oracle.genai.core.interceptor.OciSigningInterceptor; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; + +/** + * Factory for creating OkHttp clients pre-configured with OCI signing and header interceptors. + * + *

Provider modules (OpenAI, Anthropic) use this factory to obtain an OkHttpClient + * that transparently handles OCI IAM authentication on every request. + */ +public final class OciHttpClientFactory { + + private static final Logger LOG = LoggerFactory.getLogger(OciHttpClientFactory.class); + private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(2); + + private OciHttpClientFactory() { + // utility class + } + + /** + * Creates an OkHttpClient with OCI signing and header injection. + * + * @param authProvider the OCI authentication provider + * @param compartmentId the OCI compartment OCID (may be null) + * @param additionalHeaders extra headers to inject on every request + * @param timeout request timeout (null for default 2 minutes) + * @param logRequests whether to log request/response bodies + * @return a configured OkHttpClient + */ + public static OkHttpClient create( + BasicAuthenticationDetailsProvider authProvider, + String compartmentId, + Map additionalHeaders, + Duration timeout, + boolean logRequests) { + + Objects.requireNonNull(authProvider, "authProvider must not be null"); + + Duration resolvedTimeout = timeout != null ? timeout : DEFAULT_TIMEOUT; + + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .addInterceptor(new OciHeaderInterceptor(compartmentId, additionalHeaders)) + .addInterceptor(new OciSigningInterceptor(authProvider)) + .connectTimeout(resolvedTimeout) + .readTimeout(resolvedTimeout) + .writeTimeout(resolvedTimeout); + + if (logRequests) { + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor( + message -> LOG.debug("{}", message)); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + builder.addInterceptor(loggingInterceptor); + } + + LOG.debug("Created OCI-signed OkHttpClient [compartmentId={}, timeout={}]", + compartmentId, resolvedTimeout); + + return builder.build(); + } + + /** + * Creates an OkHttpClient with OCI signing, compartment header, and default settings. + */ + public static OkHttpClient create( + BasicAuthenticationDetailsProvider authProvider, + String compartmentId) { + return create(authProvider, compartmentId, null, null, false); + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java new file mode 100644 index 0000000..26bfa52 --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.auth; + +/** + * Thrown when an OCI authentication provider cannot be created or + * when request signing fails. + */ +public class OciAuthException extends RuntimeException { + + public OciAuthException(String message) { + super(message); + } + + public OciAuthException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java new file mode 100644 index 0000000..af4f5e2 --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.auth; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider; + +import java.io.IOException; + +/** + * Factory that creates OCI {@link BasicAuthenticationDetailsProvider} instances + * based on the requested authentication type. + * + *

Supported auth types: + *

    + *
  • {@code oci_config} — User principal from {@code ~/.oci/config}
  • + *
  • {@code security_token} — Session token from OCI CLI session
  • + *
  • {@code instance_principal} — For OCI Compute instances
  • + *
  • {@code resource_principal} — For OCI Functions, Container Instances, etc.
  • + *
+ */ +public final class OciAuthProviderFactory { + + public static final String AUTH_TYPE_OCI_CONFIG = "oci_config"; + public static final String AUTH_TYPE_SECURITY_TOKEN = "security_token"; + public static final String AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal"; + public static final String AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal"; + + private static final String DEFAULT_PROFILE = "DEFAULT"; + + private OciAuthProviderFactory() { + // utility class + } + + /** + * Creates an authentication provider for the given auth type. + * + * @param authType the OCI authentication type + * @param profile the OCI config profile name (used for oci_config and security_token) + * @return a configured {@link BasicAuthenticationDetailsProvider} + * @throws IllegalArgumentException if authType is not recognized + * @throws OciAuthException if the provider cannot be created + */ + public static BasicAuthenticationDetailsProvider create(String authType, String profile) { + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException("authType must not be null or blank"); + } + + String resolvedProfile = (profile == null || profile.isBlank()) ? DEFAULT_PROFILE : profile; + + return switch (authType) { + case AUTH_TYPE_OCI_CONFIG -> createConfigProvider(resolvedProfile); + case AUTH_TYPE_SECURITY_TOKEN -> createSessionTokenProvider(resolvedProfile); + case AUTH_TYPE_INSTANCE_PRINCIPAL -> createInstancePrincipalProvider(); + case AUTH_TYPE_RESOURCE_PRINCIPAL -> createResourcePrincipalProvider(); + default -> throw new IllegalArgumentException( + "Unsupported authType: '" + authType + "'. " + + "Supported values: oci_config, security_token, instance_principal, resource_principal"); + }; + } + + /** + * Creates an authentication provider for the given auth type using the DEFAULT profile. + */ + public static BasicAuthenticationDetailsProvider create(String authType) { + return create(authType, DEFAULT_PROFILE); + } + + private static BasicAuthenticationDetailsProvider createConfigProvider(String profile) { + try { + return new ConfigFileAuthenticationDetailsProvider(profile); + } catch (IOException e) { + throw new OciAuthException( + "Failed to create OCI config auth provider for profile '" + profile + "'. " + + "Ensure ~/.oci/config exists and the profile is valid.", e); + } + } + + private static BasicAuthenticationDetailsProvider createSessionTokenProvider(String profile) { + try { + return new SessionTokenAuthenticationDetailsProvider(profile); + } catch (IOException e) { + throw new OciAuthException( + "Failed to create session token auth provider for profile '" + profile + "'. " + + "Run 'oci session authenticate' to create a session.", e); + } + } + + private static BasicAuthenticationDetailsProvider createInstancePrincipalProvider() { + try { + return InstancePrincipalsAuthenticationDetailsProvider.builder().build(); + } catch (Exception e) { + throw new OciAuthException( + "Failed to create instance principal auth provider. " + + "Ensure this code is running on an OCI Compute instance with a configured dynamic group.", e); + } + } + + private static BasicAuthenticationDetailsProvider createResourcePrincipalProvider() { + try { + return ResourcePrincipalAuthenticationDetailsProvider.builder().build(); + } catch (Exception e) { + throw new OciAuthException( + "Failed to create resource principal auth provider. " + + "Ensure this code is running in an OCI Function or Container Instance with RP configured.", e); + } + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java b/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java new file mode 100644 index 0000000..2eae764 --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.endpoint; + +/** + * Resolves the OCI Generative AI service base URL from region, service endpoint, + * or an explicit base URL override. + * + *

Resolution priority (highest to lowest): + *

    + *
  1. {@code baseUrl} — fully qualified URL, used as-is
  2. + *
  3. {@code serviceEndpoint} — service root; the provider-specific API path is appended
  4. + *
  5. {@code region} — auto-derives the service endpoint from the OCI region code
  6. + *
+ */ +public final class OciEndpointResolver { + + private static final String SERVICE_ENDPOINT_TEMPLATE = + "https://inference.generativeai.%s.oci.oraclecloud.com"; + + private OciEndpointResolver() { + // utility class + } + + /** + * Resolves the base URL for an OpenAI-compatible endpoint. + * Appends {@code /openai/v1} to the service endpoint. + */ + public static String resolveOpenAiBaseUrl(String region, String serviceEndpoint, String baseUrl) { + return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/openai/v1"); + } + + /** + * Resolves the base URL for an Anthropic-compatible endpoint. + * Appends {@code /20231130/anthropic} to the service endpoint. + */ + public static String resolveAnthropicBaseUrl(String region, String serviceEndpoint, String baseUrl) { + return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/anthropic"); + } + + /** + * Resolves a base URL with a custom API path suffix. + * + * @param region OCI region code (e.g., "us-chicago-1") + * @param serviceEndpoint service root URL (without API path) + * @param baseUrl fully qualified URL override + * @param apiPath the API path to append to the service endpoint + * @return the resolved base URL + * @throws IllegalArgumentException if none of region, serviceEndpoint, or baseUrl is provided + */ + public static String resolveBaseUrl(String region, String serviceEndpoint, String baseUrl, String apiPath) { + if (baseUrl != null && !baseUrl.isBlank()) { + return stripTrailingSlash(baseUrl); + } + + if (serviceEndpoint != null && !serviceEndpoint.isBlank()) { + return stripTrailingSlash(serviceEndpoint) + apiPath; + } + + if (region != null && !region.isBlank()) { + return String.format(SERVICE_ENDPOINT_TEMPLATE, region) + apiPath; + } + + throw new IllegalArgumentException( + "At least one of region, serviceEndpoint, or baseUrl must be provided."); + } + + /** + * Builds the service endpoint URL from a region code. + */ + public static String buildServiceEndpoint(String region) { + if (region == null || region.isBlank()) { + throw new IllegalArgumentException("region must not be null or blank"); + } + return String.format(SERVICE_ENDPOINT_TEMPLATE, region); + } + + private static String stripTrailingSlash(String url) { + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java new file mode 100644 index 0000000..b23709c --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.interceptor; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * OkHttp {@link Interceptor} that injects OCI-specific headers into every request. + * + *

This handles the {@code CompartmentId} and {@code opc-compartment-id} headers + * required by the OCI Generative AI service, as well as any additional custom headers. + */ +public class OciHeaderInterceptor implements Interceptor { + + public static final String COMPARTMENT_ID_HEADER = "CompartmentId"; + public static final String OPC_COMPARTMENT_ID_HEADER = "opc-compartment-id"; + + private final Map headers; + + /** + * Creates a header interceptor with compartment ID and optional extra headers. + * + * @param compartmentId the OCI compartment OCID (may be null if not required) + * @param additionalHeaders extra headers to inject on every request + */ + public OciHeaderInterceptor(String compartmentId, Map additionalHeaders) { + var headerMap = new java.util.LinkedHashMap(); + + if (compartmentId != null && !compartmentId.isBlank()) { + headerMap.put(COMPARTMENT_ID_HEADER, compartmentId); + headerMap.put(OPC_COMPARTMENT_ID_HEADER, compartmentId); + } + + if (additionalHeaders != null) { + headerMap.putAll(additionalHeaders); + } + + this.headers = Collections.unmodifiableMap(headerMap); + } + + /** + * Creates a header interceptor with compartment ID only. + */ + public OciHeaderInterceptor(String compartmentId) { + this(compartmentId, null); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + for (Map.Entry entry : headers.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + return chain.proceed(builder.build()); + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java new file mode 100644 index 0000000..44bc6b3 --- /dev/null +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.interceptor; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.bmc.http.signing.DefaultRequestSigner; +import com.oracle.bmc.http.signing.RequestSigner; +import com.oracle.bmc.http.signing.SigningStrategy; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okio.Buffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * OkHttp {@link Interceptor} that applies OCI request signing to every outgoing request. + * + *

This interceptor computes the OCI signature (RSA-SHA256 with timestamp, nonce, + * and body digest) and injects the {@code Authorization}, {@code date}, + * {@code (request-target)}, {@code host}, and content-related headers required by + * the OCI API gateway. + * + *

Thread-safe: this interceptor can be shared across multiple OkHttp clients. + * Token refresh for {@code security_token} and principal-based auth is handled + * transparently by the underlying {@link BasicAuthenticationDetailsProvider}. + */ +public class OciSigningInterceptor implements Interceptor { + + private static final Logger LOG = LoggerFactory.getLogger(OciSigningInterceptor.class); + + private final RequestSigner requestSigner; + + /** + * Creates a signing interceptor using the given OCI auth provider. + * + * @param authProvider the OCI authentication details provider (must not be null) + */ + public OciSigningInterceptor(BasicAuthenticationDetailsProvider authProvider) { + Objects.requireNonNull(authProvider, "authProvider must not be null"); + this.requestSigner = DefaultRequestSigner.createRequestSigner( + authProvider, SigningStrategy.STANDARD); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + + URI uri = originalRequest.url().uri(); + String method = originalRequest.method(); + + // Build the headers map that OCI signing expects + Map> existingHeaders = new HashMap<>(); + for (String name : originalRequest.headers().names()) { + existingHeaders.put(name, originalRequest.headers(name)); + } + + // Read the request body for signing (OCI signs the body digest) + byte[] bodyBytes = null; + if (originalRequest.body() != null) { + Buffer buffer = new Buffer(); + originalRequest.body().writeTo(buffer); + bodyBytes = buffer.readByteArray(); + } + + // Compute OCI signature headers + Map signedHeaders; + if (bodyBytes != null && bodyBytes.length > 0) { + signedHeaders = requestSigner.signRequest( + uri, method, existingHeaders, + new java.io.ByteArrayInputStream(bodyBytes)); + } else { + signedHeaders = requestSigner.signRequest( + uri, method, existingHeaders, null); + } + + // Build a new request with all signed headers applied + Request.Builder signedRequestBuilder = originalRequest.newBuilder(); + for (Map.Entry entry : signedHeaders.entrySet()) { + signedRequestBuilder.header(entry.getKey(), entry.getValue()); + } + + // Re-attach the body (it was consumed during signing) + if (bodyBytes != null) { + MediaType contentType = originalRequest.body() != null + ? originalRequest.body().contentType() + : MediaType.parse("application/json"); + signedRequestBuilder.method(method, RequestBody.create(bodyBytes, contentType)); + } + + Request signedRequest = signedRequestBuilder.build(); + LOG.debug("OCI-signed request: {} {}", method, uri); + + return chain.proceed(signedRequest); + } +} diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java b/oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java new file mode 100644 index 0000000..a1699a2 --- /dev/null +++ b/oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.auth; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OciAuthProviderFactoryTest { + + @Test + void create_throwsOnNullAuthType() { + assertThrows(IllegalArgumentException.class, () -> + OciAuthProviderFactory.create(null)); + } + + @Test + void create_throwsOnBlankAuthType() { + assertThrows(IllegalArgumentException.class, () -> + OciAuthProviderFactory.create(" ")); + } + + @Test + void create_throwsOnUnsupportedAuthType() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> + OciAuthProviderFactory.create("unknown_type")); + assertTrue(ex.getMessage().contains("Unsupported authType")); + assertTrue(ex.getMessage().contains("unknown_type")); + } + + @Test + void create_ociConfigThrowsGracefullyWhenNoConfigFile() { + // When ~/.oci/config doesn't exist or profile is invalid, + // we expect an OciAuthException (not a raw IOException) + assertThrows(OciAuthException.class, () -> + OciAuthProviderFactory.create("oci_config", "NONEXISTENT_PROFILE_XYZ")); + } + + @Test + void create_securityTokenThrowsGracefullyWhenNoSession() { + assertThrows(OciAuthException.class, () -> + OciAuthProviderFactory.create("security_token", "NONEXISTENT_PROFILE_XYZ")); + } +} diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java b/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java new file mode 100644 index 0000000..2f94c00 --- /dev/null +++ b/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.endpoint; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OciEndpointResolverTest { + + @Test + void resolveOpenAiBaseUrl_fromRegion() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl("us-chicago-1", null, null); + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1", + url); + } + + @Test + void resolveOpenAiBaseUrl_fromServiceEndpoint() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl( + null, "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", null); + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1", + url); + } + + @Test + void resolveOpenAiBaseUrl_fromBaseUrl() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl( + null, null, "https://custom-endpoint.example.com/openai/v1"); + assertEquals("https://custom-endpoint.example.com/openai/v1", url); + } + + @Test + void resolveOpenAiBaseUrl_baseUrlTakesPrecedence() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl( + "us-chicago-1", + "https://service.example.com", + "https://override.example.com/v1"); + assertEquals("https://override.example.com/v1", url); + } + + @Test + void resolveOpenAiBaseUrl_serviceEndpointTakesPrecedenceOverRegion() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl( + "us-chicago-1", + "https://custom-service.example.com", + null); + assertEquals("https://custom-service.example.com/20231130/openai/v1", url); + } + + @Test + void resolveOpenAiBaseUrl_stripsTrailingSlash() { + String url = OciEndpointResolver.resolveOpenAiBaseUrl( + null, "https://service.example.com/", null); + assertEquals("https://service.example.com/20231130/openai/v1", url); + } + + @Test + void resolveAnthropicBaseUrl_fromRegion() { + String url = OciEndpointResolver.resolveAnthropicBaseUrl("us-chicago-1", null, null); + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/anthropic", + url); + } + + @Test + void resolveBaseUrl_throwsWhenNothingProvided() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveOpenAiBaseUrl(null, null, null)); + } + + @Test + void buildServiceEndpoint_fromRegion() { + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", + OciEndpointResolver.buildServiceEndpoint("us-chicago-1")); + } + + @Test + void buildServiceEndpoint_throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.buildServiceEndpoint(null)); + } +} diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java b/oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java new file mode 100644 index 0000000..f84e747 --- /dev/null +++ b/oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.core.interceptor; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class OciHeaderInterceptorTest { + + private MockWebServer server; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + void injectsCompartmentIdHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(new OciHeaderInterceptor("ocid1.compartment.oc1..test")) + .build(); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertEquals("ocid1.compartment.oc1..test", request.getHeader("CompartmentId")); + assertEquals("ocid1.compartment.oc1..test", request.getHeader("opc-compartment-id")); + } + + @Test + void injectsAdditionalHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(new OciHeaderInterceptor( + "ocid1.compartment.oc1..test", + Map.of("opc-conversation-store-id", "ocid1.store.oc1..test"))) + .build(); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertEquals("ocid1.compartment.oc1..test", request.getHeader("CompartmentId")); + assertEquals("ocid1.store.oc1..test", request.getHeader("opc-conversation-store-id")); + } + + @Test + void skipsCompartmentHeadersWhenNull() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(new OciHeaderInterceptor(null)) + .build(); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertNull(request.getHeader("CompartmentId")); + assertNull(request.getHeader("opc-compartment-id")); + } +} diff --git a/oci-genai-openai/pom.xml b/oci-genai-openai/pom.xml new file mode 100644 index 0000000..2375ee1 --- /dev/null +++ b/oci-genai-openai/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + + com.oracle.genai + oci-genai-parent + 0.1.0-SNAPSHOT + + + oci-genai-openai + jar + + OCI GenAI SDK :: OpenAI + + OpenAI provider module for the OCI GenAI SDK. + Wraps the official openai-java SDK with OCI IAM authentication + via OkHttp interceptor, targeting OCI Generative AI endpoints. + + + + + + com.oracle.genai + oci-genai-core + + + + + com.openai + openai-java + + + diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java new file mode 100644 index 0000000..274fe18 --- /dev/null +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import com.openai.client.OpenAIClientAsync; +import com.openai.client.OpenAIClientAsyncImpl; +import com.openai.core.ClientOptions; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.genai.core.OciHttpClientFactory; +import com.oracle.genai.core.auth.OciAuthProviderFactory; +import com.oracle.genai.core.endpoint.OciEndpointResolver; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Async OCI-authenticated OpenAI client builder. + * + *

Creates an {@link OpenAIClientAsync} that routes requests through OCI + * Generative AI endpoints with OCI IAM request signing. Supports async/await + * patterns using {@link java.util.concurrent.CompletableFuture}. + * + *

Quick Start

+ *
{@code
+ * OpenAIClientAsync client = AsyncOciOpenAI.builder()
+ *         .compartmentId("")
+ *         .authType("security_token")
+ *         .region("us-chicago-1")
+ *         .build();
+ *
+ * client.responses().create(ResponseCreateParams.builder()
+ *         .model("openai.gpt-4o")
+ *         .store(false)
+ *         .input("Write a short poem about cloud computing.")
+ *         .build())
+ *     .thenAccept(response -> System.out.println(response.output()));
+ * }
+ */ +public final class AsyncOciOpenAI { + + private static final String CONVERSATION_STORE_ID_HEADER = "opc-conversation-store-id"; + + private AsyncOciOpenAI() { + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String authType; + private String profile; + private BasicAuthenticationDetailsProvider authProvider; + private String compartmentId; + private String conversationStoreId; + private String region; + private String serviceEndpoint; + private String baseUrl; + private Duration timeout; + private boolean logRequestsAndResponses; + + private Builder() { + } + + public Builder authType(String authType) { this.authType = authType; return this; } + public Builder profile(String profile) { this.profile = profile; return this; } + public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } + public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } + public Builder conversationStoreId(String conversationStoreId) { this.conversationStoreId = conversationStoreId; return this; } + public Builder region(String region) { this.region = region; return this; } + public Builder serviceEndpoint(String serviceEndpoint) { this.serviceEndpoint = serviceEndpoint; return this; } + public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } + public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } + public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } + + public OpenAIClientAsync build() { + BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); + + String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( + region, serviceEndpoint, baseUrl); + + if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { + throw new IllegalArgumentException( + "compartmentId is required to access the OCI Generative AI Service."); + } + + Map additionalHeaders = buildAdditionalHeaders(); + + okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( + resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); + + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(resolvedBaseUrl) + .apiKey("OCI_AUTH_NOT_USED") + .build(); + + return new OpenAIClientAsyncImpl(clientOptions); + } + + private Map buildAdditionalHeaders() { + Map headers = new LinkedHashMap<>(); + if (conversationStoreId != null && !conversationStoreId.isBlank()) { + headers.put(CONVERSATION_STORE_ID_HEADER, conversationStoreId); + } + return headers.isEmpty() ? null : headers; + } + + private BasicAuthenticationDetailsProvider resolveAuthProvider() { + if (authProvider != null) return authProvider; + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException("Either authType or authProvider must be provided."); + } + return OciAuthProviderFactory.create(authType, profile); + } + } +} diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java new file mode 100644 index 0000000..a9b00f4 --- /dev/null +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.genai.core.OciHttpClientFactory; +import com.oracle.genai.core.auth.OciAuthProviderFactory; +import com.oracle.genai.core.endpoint.OciEndpointResolver; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * OCI-authenticated OpenAI client builder. + * + *

Creates an {@link OpenAIClient} that routes requests through OCI Generative AI + * endpoints with OCI IAM request signing. The underlying OpenAI Java SDK is used + * for all API operations — users get the full OpenAI API surface (chat completions, + * responses, embeddings) with OCI auth handled transparently. + * + *

Quick Start

+ *
{@code
+ * OpenAIClient client = OciOpenAI.builder()
+ *         .compartmentId("")
+ *         .authType("security_token")
+ *         .profile("DEFAULT")
+ *         .region("us-chicago-1")
+ *         .build();
+ *
+ * Response response = client.responses().create(ResponseCreateParams.builder()
+ *         .model("openai.gpt-4o")
+ *         .store(false)
+ *         .input("Write a short poem about cloud computing.")
+ *         .build());
+ * }
+ * + *

Authentication

+ *

Supports all OCI IAM auth types via {@code authType}: + *

    + *
  • {@code oci_config} — user principal from {@code ~/.oci/config}
  • + *
  • {@code security_token} — session token from OCI CLI
  • + *
  • {@code instance_principal} — OCI Compute instances
  • + *
  • {@code resource_principal} — OCI Functions, Container Instances
  • + *
+ *

Alternatively, pass a pre-built {@link BasicAuthenticationDetailsProvider} + * via {@code authProvider()}. + */ +public final class OciOpenAI { + + /** Header key for conversation store OCID. */ + private static final String CONVERSATION_STORE_ID_HEADER = "opc-conversation-store-id"; + + private OciOpenAI() { + // static builder entry point only + } + + /** + * Returns a new builder for configuring an OCI-authenticated OpenAI client. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String authType; + private String profile; + private BasicAuthenticationDetailsProvider authProvider; + private String compartmentId; + private String conversationStoreId; + private String region; + private String serviceEndpoint; + private String baseUrl; + private Duration timeout; + private boolean logRequestsAndResponses; + + private Builder() { + } + + /** + * Sets the OCI authentication type. + * One of: {@code oci_config}, {@code security_token}, + * {@code instance_principal}, {@code resource_principal}. + */ + public Builder authType(String authType) { + this.authType = authType; + return this; + } + + /** + * Sets the OCI config profile name. Used with {@code oci_config} and + * {@code security_token} auth types. Defaults to {@code "DEFAULT"}. + */ + public Builder profile(String profile) { + this.profile = profile; + return this; + } + + /** + * Sets a pre-built OCI authentication provider. + * When set, {@code authType} and {@code profile} are ignored. + */ + public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { + this.authProvider = authProvider; + return this; + } + + /** + * Sets the OCI compartment OCID. Required for OCI Generative AI endpoints. + */ + public Builder compartmentId(String compartmentId) { + this.compartmentId = compartmentId; + return this; + } + + /** + * Sets the optional Conversation Store OCID, attached to every request + * as the {@code opc-conversation-store-id} header. + */ + public Builder conversationStoreId(String conversationStoreId) { + this.conversationStoreId = conversationStoreId; + return this; + } + + /** + * Sets the OCI region code (e.g., {@code "us-chicago-1"}). + * Auto-derives the service endpoint URL. + */ + public Builder region(String region) { + this.region = region; + return this; + } + + /** + * Sets the OCI service endpoint (without API path). + * {@code /openai/v1} is appended automatically. + * Takes precedence over {@code region}. + */ + public Builder serviceEndpoint(String serviceEndpoint) { + this.serviceEndpoint = serviceEndpoint; + return this; + } + + /** + * Sets the fully qualified base URL (including API path). + * Used as-is without modification. + * Takes precedence over {@code serviceEndpoint} and {@code region}. + */ + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Sets the request timeout. Defaults to 2 minutes. + */ + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Enables debug logging of request/response bodies. + */ + public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { + this.logRequestsAndResponses = logRequestsAndResponses; + return this; + } + + /** + * Builds the OCI-authenticated OpenAI client. + * + * @return a configured {@link OpenAIClient} + * @throws IllegalArgumentException if required parameters are missing + */ + public OpenAIClient build() { + // Resolve auth provider + BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); + + // Resolve base URL + String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( + region, serviceEndpoint, baseUrl); + + // Validate compartment ID for GenAI endpoints + if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { + throw new IllegalArgumentException( + "compartmentId is required to access the OCI Generative AI Service."); + } + + // Build additional headers (conversation store ID) + Map additionalHeaders = buildAdditionalHeaders(); + + // Create OCI-signed OkHttpClient from core + okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( + resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); + + // Create a signing HTTP client that implements the OpenAI SDK's HttpClient interface. + // This bridges the OCI-signed OkHttpClient with the SDK's HTTP abstraction. + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + + // Build ClientOptions with our signing HTTP client, base URL, and a dummy API key. + // OCI signing replaces API key authentication entirely. + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(resolvedBaseUrl) + .apiKey("OCI_AUTH_NOT_USED") + .build(); + + return new OpenAIClientImpl(clientOptions); + } + + private Map buildAdditionalHeaders() { + Map headers = new LinkedHashMap<>(); + if (conversationStoreId != null && !conversationStoreId.isBlank()) { + headers.put(CONVERSATION_STORE_ID_HEADER, conversationStoreId); + } + return headers.isEmpty() ? null : headers; + } + + private BasicAuthenticationDetailsProvider resolveAuthProvider() { + if (authProvider != null) { + return authProvider; + } + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException( + "Either authType or authProvider must be provided."); + } + return OciAuthProviderFactory.create(authType, profile); + } + } +} diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java new file mode 100644 index 0000000..7b66b47 --- /dev/null +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import com.openai.core.RequestOptions; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; +import com.openai.core.http.Headers; +import okhttp3.*; +import okio.BufferedSink; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; + +/** + * An implementation of the OpenAI SDK's {@link HttpClient} interface backed by + * an OCI-signed {@link okhttp3.OkHttpClient}. + * + *

This bridges the OpenAI SDK's HTTP abstraction with OCI request signing. + * The underlying OkHttpClient has {@code OciSigningInterceptor} and + * {@code OciHeaderInterceptor} already configured, so every request is + * automatically signed with OCI IAM credentials. + */ +class OciSigningHttpClient implements HttpClient { + + private static final Logger LOG = LoggerFactory.getLogger(OciSigningHttpClient.class); + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); + + private final okhttp3.OkHttpClient okHttpClient; + + OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + try { + Response okResponse = okHttpClient.newCall(okRequest).execute(); + return new OkHttpResponseAdapter(okResponse); + } catch (IOException e) { + throw new RuntimeException("OCI request failed: " + request.url(), e); + } + } + + @Override + public CompletableFuture executeAsync( + HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + CompletableFuture future = new CompletableFuture<>(); + + okHttpClient.newCall(okRequest).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally( + new RuntimeException("OCI async request failed: " + request.url(), e)); + } + + @Override + public void onResponse(Call call, Response response) { + future.complete(new OkHttpResponseAdapter(response)); + } + }); + + return future; + } + + @Override + public void close() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + + private Request toOkHttpRequest(HttpRequest request) { + // Build the full URL from the SDK request + String url = request.url(); + HttpUrl parsedUrl = HttpUrl.parse(url); + if (parsedUrl == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + + HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); + + // Add query params + var queryParams = request.queryParams(); + for (String key : queryParams.keys()) { + for (String value : queryParams.values(key)) { + urlBuilder.addQueryParameter(key, value); + } + } + + // Build headers + okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); + var headers = request.headers(); + for (String name : headers.names()) { + for (String value : headers.values(name)) { + headersBuilder.add(name, value); + } + } + + // Build request body + RequestBody body = null; + HttpRequestBody requestBody = request.body(); + if (requestBody != null) { + body = new RequestBody() { + @Override + public MediaType contentType() { + String ct = requestBody.contentType(); + return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return requestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (OutputStream os = sink.outputStream()) { + requestBody.writeTo(os); + } + } + }; + } + + String method = request.method().name(); + return new Request.Builder() + .url(urlBuilder.build()) + .headers(headersBuilder.build()) + .method(method, body) + .build(); + } + + /** + * Adapts an OkHttp {@link Response} to the OpenAI SDK's {@link HttpResponse} interface. + */ + private static class OkHttpResponseAdapter implements HttpResponse { + + private final Response response; + private final Headers headers; + + OkHttpResponseAdapter(Response response) { + this.response = response; + // Convert OkHttp headers to SDK Headers + Headers.Builder builder = Headers.builder(); + for (String name : response.headers().names()) { + for (String value : response.headers(name)) { + builder.put(name, value); + } + } + this.headers = builder.build(); + } + + @Override + public int statusCode() { + return response.code(); + } + + @Override + public Headers headers() { + return headers; + } + + @Override + public InputStream body() { + ResponseBody responseBody = response.body(); + return responseBody != null ? responseBody.byteStream() : InputStream.nullInputStream(); + } + + @Override + public void close() { + response.close(); + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..42c282f --- /dev/null +++ b/pom.xml @@ -0,0 +1,139 @@ + + + + 4.0.0 + + com.oracle.genai + oci-genai-parent + 0.1.0-SNAPSHOT + pom + + OCI GenAI SDK :: Parent + + Unified Java SDK family for integrating third-party Generative AI providers + (OpenAI, Anthropic) with Oracle Cloud Infrastructure authentication and routing. + + https://github.com/oracle/oci-genai-java-sdk + + + + Universal Permissive License v 1.0 + https://oss.oracle.com/licenses/upl/ + + + + + oci-genai-bom + oci-genai-core + oci-genai-openai + oci-genai-anthropic + + + + UTF-8 + 17 + 17 + + + 0.1.0-SNAPSHOT + + + 3.57.2 + + + 0.40.0 + 2.12.0 + + + 4.12.0 + 2.18.2 + 2.0.16 + + + 5.11.4 + 5.14.2 + + + 3.13.0 + 3.5.2 + 3.4.2 + + + + + + + com.oracle.genai + oci-genai-bom + ${oci-genai.version} + pom + import + + + + + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.squareup.okhttp3 + mockwebserver + ${okhttp.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + From 0b5a6618b22191fa5db865dff05e04031112127f Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Fri, 20 Feb 2026 10:47:34 -0800 Subject: [PATCH 02/16] Add API key auth support and Anthropic integration tests When .apiKey("sk-...") is set, builders now create native SDK clients with direct API key authentication, bypassing OCI IAM signing entirely. This supports the Dev endpoint's x-api-key auth mode. Both OCI IAM and API key paths are available from the same builder: - OCI IAM: .authType("security_token") with signing interceptors - API key: .apiKey("sk-...") using native AnthropicOkHttpClient/OpenAIOkHttpClient Verified working against Dev endpoint with API key auth. Co-Authored-By: Claude Opus 4.6 --- .../genai/anthropic/AsyncOciAnthropic.java | 38 +++++++- .../oracle/genai/anthropic/OciAnthropic.java | 74 +++++++++++--- .../anthropic/AnthropicIntegrationTest.java | 97 +++++++++++++++++++ .../oracle/genai/openai/AsyncOciOpenAI.java | 38 +++++++- .../com/oracle/genai/openai/OciOpenAI.java | 65 ++++++++++--- 5 files changed, 287 insertions(+), 25 deletions(-) create mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java index fda6a25..666996e 100644 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java @@ -7,6 +7,7 @@ import com.anthropic.client.AnthropicClientAsync; import com.anthropic.client.AnthropicClientAsyncImpl; +import com.anthropic.client.okhttp.AnthropicOkHttpClientAsync; import com.anthropic.core.ClientOptions; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.genai.core.OciHttpClientFactory; @@ -49,6 +50,7 @@ public static Builder builder() { public static final class Builder { private String authType; private String profile; + private String apiKey; private BasicAuthenticationDetailsProvider authProvider; private String compartmentId; private String region; @@ -62,6 +64,7 @@ private Builder() { public Builder authType(String authType) { this.authType = authType; return this; } public Builder profile(String profile) { this.profile = profile; return this; } + public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } public Builder region(String region) { this.region = region; return this; } @@ -71,6 +74,39 @@ private Builder() { public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } public AnthropicClientAsync build() { + if (isApiKeyMode()) { + return buildApiKeyClient(); + } + return buildOciSignedClient(); + } + + private boolean isApiKeyMode() { + return (apiKey != null && !apiKey.isBlank()) + || "api_key".equals(authType); + } + + private AnthropicClientAsync buildApiKeyClient() { + String resolvedApiKey = apiKey; + if (resolvedApiKey == null || resolvedApiKey.isBlank()) { + throw new IllegalArgumentException( + "apiKey is required when authType is 'api_key'."); + } + + String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( + region, serviceEndpoint, baseUrl); + + AnthropicOkHttpClientAsync.Builder builder = AnthropicOkHttpClientAsync.builder() + .apiKey(resolvedApiKey) + .baseUrl(resolvedBaseUrl); + + if (timeout != null) { + builder.timeout(timeout); + } + + return builder.build(); + } + + private AnthropicClientAsync buildOciSignedClient() { BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( @@ -97,7 +133,7 @@ public AnthropicClientAsync build() { private BasicAuthenticationDetailsProvider resolveAuthProvider() { if (authProvider != null) return authProvider; if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException("Either authType or authProvider must be provided."); + throw new IllegalArgumentException("Either authType, authProvider, or apiKey must be provided."); } return OciAuthProviderFactory.create(authType, profile); } diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java index 174b5fe..23c3a52 100644 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java @@ -7,6 +7,7 @@ import com.anthropic.client.AnthropicClient; import com.anthropic.client.AnthropicClientImpl; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.ClientOptions; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.genai.core.OciHttpClientFactory; @@ -45,9 +46,10 @@ *

  • {@code security_token} — session token from OCI CLI
  • *
  • {@code instance_principal} — OCI Compute instances
  • *
  • {@code resource_principal} — OCI Functions, Container Instances
  • + *
  • {@code api_key} — direct API key authentication (no OCI signing)
  • * *

    Alternatively, pass a pre-built {@link BasicAuthenticationDetailsProvider} - * via {@code authProvider()}. + * via {@code authProvider()}, or use {@code apiKey()} for direct API key auth. */ public final class OciAnthropic { @@ -61,6 +63,7 @@ public static Builder builder() { public static final class Builder { private String authType; private String profile; + private String apiKey; private BasicAuthenticationDetailsProvider authProvider; private String compartmentId; private String region; @@ -75,7 +78,8 @@ private Builder() { /** * Sets the OCI authentication type. * One of: {@code oci_config}, {@code security_token}, - * {@code instance_principal}, {@code resource_principal}. + * {@code instance_principal}, {@code resource_principal}, + * {@code api_key}. */ public Builder authType(String authType) { this.authType = authType; @@ -91,6 +95,16 @@ public Builder profile(String profile) { return this; } + /** + * Sets the API key for direct authentication (no OCI signing). + * When set, requests are authenticated with this key via the + * {@code X-Api-Key} header, bypassing OCI IAM signing entirely. + */ + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + /** * Sets a pre-built OCI authentication provider. * When set, {@code authType} and {@code profile} are ignored. @@ -146,32 +160,65 @@ public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { } /** - * Builds the OCI-authenticated Anthropic client. + * Builds the Anthropic client. + * + *

    When {@code apiKey} is set (or {@code authType} is {@code "api_key"}), + * creates a native Anthropic SDK client with direct API key auth. + * Otherwise, creates an OCI-authenticated client with IAM request signing. * * @return a configured {@link AnthropicClient} * @throws IllegalArgumentException if required parameters are missing */ public AnthropicClient build() { + // API key mode: use native Anthropic SDK client directly + if (isApiKeyMode()) { + return buildApiKeyClient(); + } + + // OCI auth mode: use custom signing HTTP client + return buildOciSignedClient(); + } + + private boolean isApiKeyMode() { + return (apiKey != null && !apiKey.isBlank()) + || "api_key".equals(authType); + } + + private AnthropicClient buildApiKeyClient() { + String resolvedApiKey = apiKey; + if (resolvedApiKey == null || resolvedApiKey.isBlank()) { + throw new IllegalArgumentException( + "apiKey is required when authType is 'api_key'."); + } + + String resolvedBaseUrl = resolveBaseUrl(); + + AnthropicOkHttpClient.Builder builder = AnthropicOkHttpClient.builder() + .apiKey(resolvedApiKey) + .baseUrl(resolvedBaseUrl); + + if (timeout != null) { + builder.timeout(timeout); + } + + return builder.build(); + } + + private AnthropicClient buildOciSignedClient() { BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( - region, serviceEndpoint, baseUrl); + String resolvedBaseUrl = resolveBaseUrl(); if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { throw new IllegalArgumentException( "compartmentId is required to access the OCI Generative AI Service."); } - // Create OCI-signed OkHttpClient from core okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( resolvedAuth, compartmentId, null, timeout, logRequestsAndResponses); - // Create a signing HTTP client that implements the Anthropic SDK's HttpClient interface. - // This strips X-Api-Key headers and delegates to the OCI-signed OkHttpClient. OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); - // Build ClientOptions with our signing HTTP client and base URL. - // No API key is needed — OCI signing replaces Anthropic API key authentication. ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) .baseUrl(resolvedBaseUrl) @@ -180,13 +227,18 @@ public AnthropicClient build() { return new AnthropicClientImpl(clientOptions); } + private String resolveBaseUrl() { + return OciEndpointResolver.resolveAnthropicBaseUrl( + region, serviceEndpoint, baseUrl); + } + private BasicAuthenticationDetailsProvider resolveAuthProvider() { if (authProvider != null) { return authProvider; } if (authType == null || authType.isBlank()) { throw new IllegalArgumentException( - "Either authType or authProvider must be provided."); + "Either authType, authProvider, or apiKey must be provided."); } return OciAuthProviderFactory.create(authType, profile); } diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java new file mode 100644 index 0000000..f397240 --- /dev/null +++ b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for OciAnthropic against live endpoints. + * + *

    These tests are disabled by default. To run them, remove the {@code @Disabled} + * annotations and ensure you have: + *

      + *
    • A valid OCI config at {@code ~/.oci/config} with a session token (for PPE test)
    • + *
    • A valid API key (for Dev test)
    • + *
    + */ +class AnthropicIntegrationTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + /** + * Test against PPE endpoint with OCI session token auth. + * + *

    Equivalent Python: + *

    +     * client = OciAnthropic(
    +     *     auth=OciSessionAuth(profile_name="DEFAULT"),
    +     *     base_url="https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic",
    +     *     compartment_id="ocid1.tenancy.oc1..aaaaaaaau...",
    +     * )
    +     * 
    + */ + @Test + @Disabled("Requires live OCI credentials and PPE endpoint access") + void testPpeEndpointWithOciAuth() { + AnthropicClient client = OciAnthropic.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .baseUrl("https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic") + .build(); + + try { + Message message = client.messages().create(MessageCreateParams.builder() + .model("anthropic.claude-haiku-4-5") + .maxTokens(256) + .addUserMessage("Write a one-sentence bedtime story about a unicorn.") + .build()); + + System.out.println("PPE Response: " + message.content()); + assert !message.content().isEmpty() : "Response should not be empty"; + } finally { + client.close(); + } + } + + /** + * Test against Dev endpoint with API key auth. + * + *

    Equivalent Python: + *

    +     * client = Anthropic(
    +     *     api_key="sk-...",
    +     *     base_url="https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic",
    +     * )
    +     * 
    + */ + @Test + @Disabled("Requires valid API key and Dev endpoint access") + void testDevEndpointWithApiKey() { + AnthropicClient client = OciAnthropic.builder() + .apiKey("YOUR_API_KEY_HERE") + .baseUrl("https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic") + .build(); + + try { + Message message = client.messages().create(MessageCreateParams.builder() + .model("anthropic.claude-haiku-4-5") + .maxTokens(256) + .addUserMessage("Write a one-sentence bedtime story about a unicorn.") + .build()); + + System.out.println("Dev Response: " + message.content()); + assert !message.content().isEmpty() : "Response should not be empty"; + } finally { + client.close(); + } + } +} diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java index 274fe18..a7f8e7c 100644 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java @@ -7,6 +7,7 @@ import com.openai.client.OpenAIClientAsync; import com.openai.client.OpenAIClientAsyncImpl; +import com.openai.client.okhttp.OpenAIOkHttpClientAsync; import com.openai.core.ClientOptions; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.genai.core.OciHttpClientFactory; @@ -54,6 +55,7 @@ public static Builder builder() { public static final class Builder { private String authType; private String profile; + private String apiKey; private BasicAuthenticationDetailsProvider authProvider; private String compartmentId; private String conversationStoreId; @@ -68,6 +70,7 @@ private Builder() { public Builder authType(String authType) { this.authType = authType; return this; } public Builder profile(String profile) { this.profile = profile; return this; } + public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } public Builder conversationStoreId(String conversationStoreId) { this.conversationStoreId = conversationStoreId; return this; } @@ -78,6 +81,39 @@ private Builder() { public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } public OpenAIClientAsync build() { + if (isApiKeyMode()) { + return buildApiKeyClient(); + } + return buildOciSignedClient(); + } + + private boolean isApiKeyMode() { + return (apiKey != null && !apiKey.isBlank()) + || "api_key".equals(authType); + } + + private OpenAIClientAsync buildApiKeyClient() { + String resolvedApiKey = apiKey; + if (resolvedApiKey == null || resolvedApiKey.isBlank()) { + throw new IllegalArgumentException( + "apiKey is required when authType is 'api_key'."); + } + + String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( + region, serviceEndpoint, baseUrl); + + OpenAIOkHttpClientAsync.Builder builder = OpenAIOkHttpClientAsync.builder() + .apiKey(resolvedApiKey) + .baseUrl(resolvedBaseUrl); + + if (timeout != null) { + builder.timeout(timeout); + } + + return builder.build(); + } + + private OpenAIClientAsync buildOciSignedClient() { BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( @@ -115,7 +151,7 @@ private Map buildAdditionalHeaders() { private BasicAuthenticationDetailsProvider resolveAuthProvider() { if (authProvider != null) return authProvider; if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException("Either authType or authProvider must be provided."); + throw new IllegalArgumentException("Either authType, authProvider, or apiKey must be provided."); } return OciAuthProviderFactory.create(authType, profile); } diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java index a9b00f4..d72dcea 100644 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java @@ -7,6 +7,7 @@ import com.openai.client.OpenAIClient; import com.openai.client.OpenAIClientImpl; +import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.core.ClientOptions; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.genai.core.OciHttpClientFactory; @@ -71,6 +72,7 @@ public static Builder builder() { public static final class Builder { private String authType; private String profile; + private String apiKey; private BasicAuthenticationDetailsProvider authProvider; private String compartmentId; private String conversationStoreId; @@ -86,7 +88,8 @@ private Builder() { /** * Sets the OCI authentication type. * One of: {@code oci_config}, {@code security_token}, - * {@code instance_principal}, {@code resource_principal}. + * {@code instance_principal}, {@code resource_principal}, + * {@code api_key}. */ public Builder authType(String authType) { this.authType = authType; @@ -102,6 +105,16 @@ public Builder profile(String profile) { return this; } + /** + * Sets the API key for direct authentication (no OCI signing). + * When set, requests are authenticated with this key via the + * {@code Authorization: Bearer} header, bypassing OCI IAM signing entirely. + */ + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + /** * Sets a pre-built OCI authentication provider. * When set, {@code authType} and {@code profile} are ignored. @@ -174,38 +187,66 @@ public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { } /** - * Builds the OCI-authenticated OpenAI client. + * Builds the OpenAI client. + * + *

    When {@code apiKey} is set (or {@code authType} is {@code "api_key"}), + * creates a native OpenAI SDK client with direct API key auth. + * Otherwise, creates an OCI-authenticated client with IAM request signing. * * @return a configured {@link OpenAIClient} * @throws IllegalArgumentException if required parameters are missing */ public OpenAIClient build() { - // Resolve auth provider + if (isApiKeyMode()) { + return buildApiKeyClient(); + } + return buildOciSignedClient(); + } + + private boolean isApiKeyMode() { + return (apiKey != null && !apiKey.isBlank()) + || "api_key".equals(authType); + } + + private OpenAIClient buildApiKeyClient() { + String resolvedApiKey = apiKey; + if (resolvedApiKey == null || resolvedApiKey.isBlank()) { + throw new IllegalArgumentException( + "apiKey is required when authType is 'api_key'."); + } + + String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( + region, serviceEndpoint, baseUrl); + + OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder() + .apiKey(resolvedApiKey) + .baseUrl(resolvedBaseUrl); + + if (timeout != null) { + builder.timeout(timeout); + } + + return builder.build(); + } + + private OpenAIClient buildOciSignedClient() { BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - // Resolve base URL String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( region, serviceEndpoint, baseUrl); - // Validate compartment ID for GenAI endpoints if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { throw new IllegalArgumentException( "compartmentId is required to access the OCI Generative AI Service."); } - // Build additional headers (conversation store ID) Map additionalHeaders = buildAdditionalHeaders(); - // Create OCI-signed OkHttpClient from core okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); - // Create a signing HTTP client that implements the OpenAI SDK's HttpClient interface. - // This bridges the OCI-signed OkHttpClient with the SDK's HTTP abstraction. OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); - // Build ClientOptions with our signing HTTP client, base URL, and a dummy API key. - // OCI signing replaces API key authentication entirely. ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) .baseUrl(resolvedBaseUrl) @@ -229,7 +270,7 @@ private BasicAuthenticationDetailsProvider resolveAuthProvider() { } if (authType == null || authType.isBlank()) { throw new IllegalArgumentException( - "Either authType or authProvider must be provided."); + "Either authType, authProvider, or apiKey must be provided."); } return OciAuthProviderFactory.create(authType, profile); } From d8f67aaff66bb6040a32c4304bdd676d43686a91 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Tue, 24 Feb 2026 17:39:54 -0800 Subject: [PATCH 03/16] Fix endpoint paths, signing, and URL construction bugs found during PPE/Dev testing - Fix OpenAI endpoint path to /20231130/actions/v1 (was /20231130/openai/v1) - Fix Anthropic endpoint path to /anthropic (was /20231130/anthropic) - Fix DuplicatableInputStream in OciSigningInterceptor (use WrappedByteArrayInputStream) - Add anthropic-version header in OciAnthropic and AsyncOciAnthropic - Fix OpenAI OciSigningHttpClient to build URL from baseUrl + pathSegments - Broaden exception handling in OciAuthProviderFactory - Add unit tests for OpenAI and Anthropic modules - Add OpenAI integration test examples Co-Authored-By: Claude Opus 4.6 --- .../genai/anthropic/AsyncOciAnthropic.java | 1 + .../oracle/genai/anthropic/OciAnthropic.java | 1 + .../genai/anthropic/OciAnthropicTest.java | 82 ++++++++++++++++++ .../core/auth/OciAuthProviderFactory.java | 4 +- .../core/endpoint/OciEndpointResolver.java | 8 +- .../interceptor/OciSigningInterceptor.java | 3 +- .../endpoint/OciEndpointResolverTest.java | 10 +-- .../oracle/genai/openai/AsyncOciOpenAI.java | 2 +- .../com/oracle/genai/openai/OciOpenAI.java | 2 +- .../genai/openai/OciSigningHttpClient.java | 31 +++++-- .../oracle/genai/openai/OciOpenAITest.java | 84 +++++++++++++++++++ .../genai/openai/OpenAIIntegrationTest.java | 80 ++++++++++++++++++ 12 files changed, 287 insertions(+), 21 deletions(-) create mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java create mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java create mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java index 666996e..7bc1b36 100644 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java @@ -125,6 +125,7 @@ private AnthropicClientAsync buildOciSignedClient() { ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) .baseUrl(resolvedBaseUrl) + .putHeader("anthropic-version", "2023-06-01") .build(); return new AnthropicClientAsyncImpl(clientOptions); diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java index 23c3a52..7e031a1 100644 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java +++ b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java @@ -222,6 +222,7 @@ private AnthropicClient buildOciSignedClient() { ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) .baseUrl(resolvedBaseUrl) + .putHeader("anthropic-version", "2023-06-01") .build(); return new AnthropicClientImpl(clientOptions); diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java new file mode 100644 index 0000000..aa3fc9d --- /dev/null +++ b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link OciAnthropic} builder validation logic. + * + *

    These tests verify builder parameter validation and mode selection + * without making any network calls. + */ +class OciAnthropicTest { + + @Test + void builder_throwsWhenNoAuthProvided() { + OciAnthropic.Builder builder = OciAnthropic.builder() + .region("us-chicago-1") + .compartmentId("ocid1.compartment.oc1..test"); + + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when neither authType, authProvider, nor apiKey is provided"); + } + + @Test + void builder_throwsWhenApiKeyAuthTypeButNoKey() { + OciAnthropic.Builder builder = OciAnthropic.builder() + .authType("api_key") + .region("us-chicago-1"); + + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when authType is 'api_key' but no apiKey is set"); + } + + @Test + void builder_apiKeyModeReturnsClient() { + var client = OciAnthropic.builder() + .apiKey("sk-test-key-12345") + .baseUrl("https://example.com/anthropic") + .build(); + + assertNotNull(client, "API key mode should return a valid client"); + client.close(); + } + + @Test + void builder_apiKeyModeWithRegionReturnsClient() { + var client = OciAnthropic.builder() + .apiKey("sk-test-key-12345") + .region("us-chicago-1") + .build(); + + assertNotNull(client, "API key mode with region should return a valid client"); + client.close(); + } + + @Test + void builder_throwsWhenNoEndpointInfo() { + OciAnthropic.Builder builder = OciAnthropic.builder() + .apiKey("sk-test-key-12345"); + + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when no endpoint information is provided"); + } + + @Test + void builder_apiKeyModeWithAuthTypeReturnsClient() { + var client = OciAnthropic.builder() + .authType("api_key") + .apiKey("sk-test-key-12345") + .baseUrl("https://example.com/anthropic") + .build(); + + assertNotNull(client, "Explicit api_key authType should return a valid client"); + client.close(); + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java index af4f5e2..6b1dca6 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java @@ -75,7 +75,7 @@ public static BasicAuthenticationDetailsProvider create(String authType) { private static BasicAuthenticationDetailsProvider createConfigProvider(String profile) { try { return new ConfigFileAuthenticationDetailsProvider(profile); - } catch (IOException e) { + } catch (Exception e) { throw new OciAuthException( "Failed to create OCI config auth provider for profile '" + profile + "'. " + "Ensure ~/.oci/config exists and the profile is valid.", e); @@ -85,7 +85,7 @@ private static BasicAuthenticationDetailsProvider createConfigProvider(String pr private static BasicAuthenticationDetailsProvider createSessionTokenProvider(String profile) { try { return new SessionTokenAuthenticationDetailsProvider(profile); - } catch (IOException e) { + } catch (Exception e) { throw new OciAuthException( "Failed to create session token auth provider for profile '" + profile + "'. " + "Run 'oci session authenticate' to create a session.", e); diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java b/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java index 2eae764..eda6cb7 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java @@ -27,18 +27,18 @@ private OciEndpointResolver() { /** * Resolves the base URL for an OpenAI-compatible endpoint. - * Appends {@code /openai/v1} to the service endpoint. + * Appends {@code /20231130/actions/v1} to the service endpoint. */ public static String resolveOpenAiBaseUrl(String region, String serviceEndpoint, String baseUrl) { - return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/openai/v1"); + return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/actions/v1"); } /** * Resolves the base URL for an Anthropic-compatible endpoint. - * Appends {@code /20231130/anthropic} to the service endpoint. + * Appends {@code /anthropic} to the service endpoint. */ public static String resolveAnthropicBaseUrl(String region, String serviceEndpoint, String baseUrl) { - return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/anthropic"); + return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/anthropic"); } /** diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java index 44bc6b3..77723ba 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java @@ -9,6 +9,7 @@ import com.oracle.bmc.http.signing.DefaultRequestSigner; import com.oracle.bmc.http.signing.RequestSigner; import com.oracle.bmc.http.signing.SigningStrategy; +import com.oracle.bmc.io.internal.WrappedByteArrayInputStream; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.Request; @@ -80,7 +81,7 @@ public Response intercept(Chain chain) throws IOException { if (bodyBytes != null && bodyBytes.length > 0) { signedHeaders = requestSigner.signRequest( uri, method, existingHeaders, - new java.io.ByteArrayInputStream(bodyBytes)); + new WrappedByteArrayInputStream(bodyBytes)); } else { signedHeaders = requestSigner.signRequest( uri, method, existingHeaders, null); diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java b/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java index 2f94c00..fcad4d5 100644 --- a/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java +++ b/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java @@ -15,7 +15,7 @@ class OciEndpointResolverTest { void resolveOpenAiBaseUrl_fromRegion() { String url = OciEndpointResolver.resolveOpenAiBaseUrl("us-chicago-1", null, null); assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1", + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1", url); } @@ -24,7 +24,7 @@ void resolveOpenAiBaseUrl_fromServiceEndpoint() { String url = OciEndpointResolver.resolveOpenAiBaseUrl( null, "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", null); assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1", + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1", url); } @@ -50,21 +50,21 @@ void resolveOpenAiBaseUrl_serviceEndpointTakesPrecedenceOverRegion() { "us-chicago-1", "https://custom-service.example.com", null); - assertEquals("https://custom-service.example.com/20231130/openai/v1", url); + assertEquals("https://custom-service.example.com/20231130/actions/v1", url); } @Test void resolveOpenAiBaseUrl_stripsTrailingSlash() { String url = OciEndpointResolver.resolveOpenAiBaseUrl( null, "https://service.example.com/", null); - assertEquals("https://service.example.com/20231130/openai/v1", url); + assertEquals("https://service.example.com/20231130/actions/v1", url); } @Test void resolveAnthropicBaseUrl_fromRegion() { String url = OciEndpointResolver.resolveAnthropicBaseUrl("us-chicago-1", null, null); assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/anthropic", + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", url); } diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java index a7f8e7c..00d2829 100644 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java @@ -129,7 +129,7 @@ private OpenAIClientAsync buildOciSignedClient() { okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient, resolvedBaseUrl); ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java index d72dcea..09d4ea7 100644 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java @@ -245,7 +245,7 @@ private OpenAIClient buildOciSignedClient() { okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); + OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient, resolvedBaseUrl); ClientOptions clientOptions = ClientOptions.builder() .httpClient(signingHttpClient) diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java index 7b66b47..ca2a333 100644 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java +++ b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -36,9 +37,14 @@ class OciSigningHttpClient implements HttpClient { private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); private final okhttp3.OkHttpClient okHttpClient; + private final HttpUrl baseUrl; - OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient) { + OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient, String baseUrl) { this.okHttpClient = okHttpClient; + this.baseUrl = HttpUrl.parse(baseUrl); + if (this.baseUrl == null) { + throw new IllegalArgumentException("Invalid base URL: " + baseUrl); + } } @Override @@ -81,15 +87,26 @@ public void close() { } private Request toOkHttpRequest(HttpRequest request) { - // Build the full URL from the SDK request + // Build the full URL: baseUrl + pathSegments, or use url() if available + HttpUrl.Builder urlBuilder; String url = request.url(); - HttpUrl parsedUrl = HttpUrl.parse(url); - if (parsedUrl == null) { - throw new IllegalArgumentException("Invalid URL: " + url); + if (url != null && !url.isBlank()) { + HttpUrl parsedUrl = HttpUrl.parse(url); + if (parsedUrl == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + urlBuilder = parsedUrl.newBuilder(); + } else { + // Build from baseUrl + pathSegments (how the OpenAI SDK works) + urlBuilder = baseUrl.newBuilder(); + List pathSegments = request.pathSegments(); + if (pathSegments != null) { + for (String segment : pathSegments) { + urlBuilder.addPathSegment(segment); + } + } } - HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); - // Add query params var queryParams = request.queryParams(); for (String key : queryParams.keys()) { diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java new file mode 100644 index 0000000..4d35423 --- /dev/null +++ b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link OciOpenAI} builder validation logic. + * + *

    These tests verify builder parameter validation and mode selection + * without making any network calls. + */ +class OciOpenAITest { + + @Test + void builder_throwsWhenNoAuthProvided() { + OciOpenAI.Builder builder = OciOpenAI.builder() + .region("us-chicago-1") + .compartmentId("ocid1.compartment.oc1..test"); + + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when neither authType, authProvider, nor apiKey is provided"); + } + + @Test + void builder_throwsWhenApiKeyAuthTypeButNoKey() { + OciOpenAI.Builder builder = OciOpenAI.builder() + .authType("api_key") + .region("us-chicago-1"); + + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when authType is 'api_key' but no apiKey is set"); + } + + @Test + void builder_apiKeyModeReturnsClient() { + // API key mode should build successfully without OCI credentials + var client = OciOpenAI.builder() + .apiKey("sk-test-key-12345") + .baseUrl("https://example.com/v1") + .build(); + + assertNotNull(client, "API key mode should return a valid client"); + client.close(); + } + + @Test + void builder_apiKeyModeWithRegionReturnsClient() { + var client = OciOpenAI.builder() + .apiKey("sk-test-key-12345") + .region("us-chicago-1") + .build(); + + assertNotNull(client, "API key mode with region should return a valid client"); + client.close(); + } + + @Test + void builder_throwsWhenNoEndpointInfo() { + OciOpenAI.Builder builder = OciOpenAI.builder() + .apiKey("sk-test-key-12345"); + + // No region, serviceEndpoint, or baseUrl + assertThrows(IllegalArgumentException.class, builder::build, + "Should throw when no endpoint information is provided"); + } + + @Test + void builder_apiKeyModeWithAuthTypeReturnsClient() { + var client = OciOpenAI.builder() + .authType("api_key") + .apiKey("sk-test-key-12345") + .baseUrl("https://example.com/v1") + .build(); + + assertNotNull(client, "Explicit api_key authType should return a valid client"); + client.close(); + } +} diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java new file mode 100644 index 0000000..af61fdf --- /dev/null +++ b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import com.openai.client.OpenAIClient; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for OciOpenAI against live endpoints. + * + *

    These tests are disabled by default. To run them, remove the {@code @Disabled} + * annotations and ensure you have: + *

      + *
    • A valid OCI config at {@code ~/.oci/config} with a session token (for PPE test)
    • + *
    • A valid API key (for Dev test)
    • + *
    + */ +class OpenAIIntegrationTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + /** + * Test against PPE endpoint with OCI session token auth. + */ + @Test + @Disabled("Requires live OCI credentials and PPE endpoint access") + void testPpeEndpointWithOciAuth() { + OpenAIClient client = OciOpenAI.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .baseUrl("https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") + .build(); + + try { + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("xai.grok-3") + .addUserMessage("Write a one-sentence bedtime story about a unicorn.") + .build()); + + System.out.println("PPE Response: " + completion.choices()); + assert !completion.choices().isEmpty() : "Response should not be empty"; + } finally { + client.close(); + } + } + + /** + * Test against Dev endpoint with API key auth. + */ + @Test + @Disabled("Requires valid API key and Dev endpoint access") + void testDevEndpointWithApiKey() { + OpenAIClient client = OciOpenAI.builder() + .apiKey("YOUR_API_KEY_HERE") + .baseUrl("https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") + .build(); + + try { + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("xai.grok-3") + .addUserMessage("Write a one-sentence bedtime story about a unicorn.") + .build()); + + System.out.println("Dev Response: " + completion.choices()); + assert !completion.choices().isEmpty() : "Response should not be empty"; + } finally { + client.close(); + } + } +} From 2fc5b1caa422910bc24c88367023f449e973e334 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Wed, 25 Feb 2026 10:31:50 -0800 Subject: [PATCH 04/16] Fix DuplicatableInputStream signing bug and add live demo tests OCI SDK 3.x RequestSignerImpl requires the body InputStream to implement DuplicatableInputStream for request signing. Replaced WrappedByteArrayInputStream with a custom DuplicatableByteArrayInputStream that satisfies this contract. Added LiveDemoTest for both Anthropic and OpenAI modules demonstrating the unified SDK against PPE endpoints with session token auth. Co-Authored-By: Claude Opus 4.6 --- .../oracle/genai/anthropic/LiveDemoTest.java | 148 ++++++++++++++++++ .../interceptor/OciSigningInterceptor.java | 26 ++- .../com/oracle/genai/openai/LiveDemoTest.java | 120 ++++++++++++++ 3 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java create mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java new file mode 100644 index 0000000..c49320a --- /dev/null +++ b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.anthropic; + +import com.anthropic.client.AnthropicClient; +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * ═══════════════════════════════════════════════════════════════════ + * Phase 2 — Live Demo: Unified SDK PoC + * Anthropic via OCI GenAI + * ═══════════════════════════════════════════════════════════════════ + * + * BEFORE (what a developer does today WITHOUT this SDK): + * + * // 1. Manually create an OCI auth provider + * ConfigFileAuthenticationDetailsProvider authProvider = + * new ConfigFileAuthenticationDetailsProvider("DEFAULT"); + * + * // 2. Build a request signer for OCI IAM + * RequestSigner signer = DefaultRequestSigner.createRequestSigner( + * authProvider, SigningStrategy.STANDARD); + * + * // 3. Know the exact OCI endpoint URL per region + * String endpoint = "https://inference.generativeai.us-chicago-1" + * + ".oci.oraclecloud.com/anthropic/v1/messages"; + * + * // 4. Manually build the JSON payload + * String json = "{\"model\":\"anthropic.claude-3-sonnet\"," + * + "\"max_tokens\":1024," + * + "\"messages\":[{\"role\":\"user\"," + * + "\"content\":\"Explain OCI in one sentence\"}]}"; + * + * // 5. Build the HTTP request with compartment headers + * Request request = new Request.Builder() + * .url(endpoint) + * .addHeader("CompartmentId", compartmentId) + * .addHeader("opc-compartment-id", compartmentId) + * .addHeader("anthropic-version", "2023-06-01") + * .post(RequestBody.create(json, JSON)) + * .build(); + * + * // 6. Sign the request (compute RSA-SHA256 digest, inject Authorization header) + * Map signedHeaders = signer.signRequest( + * request.url().uri(), "POST", existingHeaders, bodyStream); + * // ... rebuild request with signed headers ... + * + * // 7. Execute and manually parse the response + * Response response = httpClient.newCall(signedRequest).execute(); + * // ... parse JSON, extract content, handle errors ... + * + * // That's ~50 lines of boilerplate BEFORE you even get to your business logic. + * + * + * AFTER (with the Unified SDK — this is ALL the developer writes): + * See the test methods below. ~10 lines total. + * ═══════════════════════════════════════════════════════════════════ + * + * To run: remove @Disabled and execute: + * mvn -pl oci-genai-anthropic test -Dtest=LiveDemoTest + */ +class LiveDemoTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + private static final String BASE_URL = + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic"; + + /** + * Demo 1 — Anthropic via OCI (session token auth, for local dev) + * + * This is the demo to run from your laptop. + * Requires: oci session authenticate (OCI CLI) + */ + @Test + // @Disabled("Remove to run live demo") + void demo_Anthropic_SessionToken() { + // ── AFTER: this is ALL the developer writes ── + AnthropicClient client = OciAnthropic.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .baseUrl(BASE_URL) + .build(); + + try { + Message message = client.messages().create(MessageCreateParams.builder() + .model("anthropic.claude-haiku-4-5") + .maxTokens(256) + .addUserMessage("Explain OCI in one sentence.") + .build()); + + System.out.println("\n══════════════════════════════════════"); + System.out.println(" Anthropic via OCI GenAI — Response"); + System.out.println("══════════════════════════════════════"); + for (ContentBlock block : message.content()) { + block.text().ifPresent(textBlock -> + System.out.println(textBlock.text())); + } + System.out.println("══════════════════════════════════════\n"); + } finally { + client.close(); + } + } + + /** + * Demo 1b — Anthropic via OCI (instance principal, for OCI Compute) + * + * This is the demo to run from an OCI VM/container. + */ + @Test + @Disabled("Remove to run live demo — requires OCI Compute instance") + void demo_Anthropic_InstancePrincipal() { + // ── AFTER: same builder, just swap authType ── + AnthropicClient client = OciAnthropic.builder() + .authType("instance_principal") + .region("us-chicago-1") + .compartmentId(COMPARTMENT_ID) + .build(); + + try { + Message message = client.messages().create(MessageCreateParams.builder() + .model("anthropic.claude-haiku-4-5") + .maxTokens(256) + .addUserMessage("Explain OCI in one sentence.") + .build()); + + System.out.println("\n══════════════════════════════════════"); + System.out.println(" Anthropic (Instance Principal)"); + System.out.println("══════════════════════════════════════"); + for (ContentBlock block : message.content()) { + block.text().ifPresent(textBlock -> + System.out.println(textBlock.text())); + } + System.out.println("══════════════════════════════════════\n"); + } finally { + client.close(); + } + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java index 77723ba..9410fb1 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java +++ b/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java @@ -9,7 +9,7 @@ import com.oracle.bmc.http.signing.DefaultRequestSigner; import com.oracle.bmc.http.signing.RequestSigner; import com.oracle.bmc.http.signing.SigningStrategy; -import com.oracle.bmc.io.internal.WrappedByteArrayInputStream; +import com.oracle.bmc.http.client.io.DuplicatableInputStream; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.Request; @@ -19,7 +19,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.util.HashMap; import java.util.List; @@ -81,7 +83,7 @@ public Response intercept(Chain chain) throws IOException { if (bodyBytes != null && bodyBytes.length > 0) { signedHeaders = requestSigner.signRequest( uri, method, existingHeaders, - new WrappedByteArrayInputStream(bodyBytes)); + new DuplicatableByteArrayInputStream(bodyBytes)); } else { signedHeaders = requestSigner.signRequest( uri, method, existingHeaders, null); @@ -106,4 +108,24 @@ public Response intercept(Chain chain) throws IOException { return chain.proceed(signedRequest); } + + /** + * A {@link ByteArrayInputStream} that implements {@link DuplicatableInputStream}, + * required by OCI SDK 3.x {@code RequestSignerImpl} for body signing. + */ + private static class DuplicatableByteArrayInputStream + extends ByteArrayInputStream implements DuplicatableInputStream { + + private final byte[] data; + + DuplicatableByteArrayInputStream(byte[] data) { + super(data); + this.data = data; + } + + @Override + public InputStream duplicate() { + return new DuplicatableByteArrayInputStream(data); + } + } } diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java new file mode 100644 index 0000000..1defe51 --- /dev/null +++ b/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.openai; + +import com.openai.client.OpenAIClient; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * ═══════════════════════════════════════════════════════════════════ + * Phase 2 — Live Demo: Unified SDK PoC + * OpenAI-compatible via OCI GenAI + * ═══════════════════════════════════════════════════════════════════ + * + * BEFORE (what a developer does today WITHOUT this SDK): + * + * // Same 50-line boilerplate as Anthropic: + * // - manual OCI auth provider setup + * // - manual request signing (RSA-SHA256) + * // - know the endpoint: .../20231130/actions/v1/chat/completions + * // - manual compartment header injection + * // - manual JSON payload construction + * // - manual response parsing + * // + * // AND the OpenAI endpoint path is DIFFERENT from Anthropic! + * // Anthropic: /anthropic/v1/messages + * // OpenAI: /20231130/actions/v1/chat/completions + * // + * // Developers have to know both. The SDK handles this automatically. + * + * + * AFTER (with the Unified SDK): + * Almost identical builder pattern — that's the point. + * See the test methods below. + * ═══════════════════════════════════════════════════════════════════ + * + * To run: remove @Disabled and execute: + * mvn -pl oci-genai-openai test -Dtest=LiveDemoTest + */ +class LiveDemoTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + private static final String BASE_URL = + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1"; + + /** + * Demo 2 — OpenAI-compatible via OCI (session token auth, for local dev) + * + * This is the demo to run from your laptop. + * Requires: oci session authenticate (OCI CLI) + */ + @Test + // @Disabled("Remove to run live demo") + void demo_OpenAI_SessionToken() { + // ── AFTER: almost identical builder — that's the point ── + OpenAIClient client = OciOpenAI.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .baseUrl(BASE_URL) + .build(); + + try { + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("xai.grok-3") + .addUserMessage("Explain OCI in one sentence.") + .build()); + + System.out.println("\n══════════════════════════════════════"); + System.out.println(" OpenAI via OCI GenAI — Response"); + System.out.println("══════════════════════════════════════"); + completion.choices().forEach(choice -> + choice.message().content().ifPresent(System.out::println)); + System.out.println("══════════════════════════════════════\n"); + } finally { + client.close(); + } + } + + /** + * Demo 2b — OpenAI-compatible via OCI (instance principal, for OCI Compute) + * + * This is the demo to run from an OCI VM/container. + */ + @Test + @Disabled("Remove to run live demo — requires OCI Compute instance") + void demo_OpenAI_InstancePrincipal() { + // ── AFTER: same builder, just swap authType ── + OpenAIClient client = OciOpenAI.builder() + .authType("instance_principal") + .region("us-chicago-1") + .compartmentId(COMPARTMENT_ID) + .build(); + + try { + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("xai.grok-3") + .addUserMessage("Explain OCI in one sentence.") + .build()); + + System.out.println("\n══════════════════════════════════════"); + System.out.println(" OpenAI (Instance Principal)"); + System.out.println("══════════════════════════════════════"); + completion.choices().forEach(choice -> + choice.message().content().ifPresent(System.out::println)); + System.out.println("══════════════════════════════════════\n"); + } finally { + client.close(); + } + } +} From bc426022727955e64af6bffb8691548cfc41c6f1 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 26 Feb 2026 17:30:13 -0800 Subject: [PATCH 05/16] Restructure to vendor-neutral auth library (oci-genai-auth-java) Replace multi-provider SDK with standalone OCI auth/signing library. Remove provider modules (openai, anthropic) and extract auth core into com.oracle.genai.auth package with OciAuthConfig builder API. Provider integrations moved to standalone examples. Co-Authored-By: Claude Opus 4.6 --- README.md | 314 +++++------------- examples/anthropic/OciAnthropicExample.java | 93 ++++++ .../OciGeminiDirectExample.java | 98 ++++++ examples/openai/OciOpenAIExample.java | 88 +++++ oci-genai-anthropic/pom.xml | 42 --- .../genai/anthropic/AsyncOciAnthropic.java | 142 -------- .../oracle/genai/anthropic/OciAnthropic.java | 247 -------------- .../genai/anthropic/OciSigningHttpClient.java | 186 ----------- .../anthropic/AnthropicIntegrationTest.java | 97 ------ .../oracle/genai/anthropic/LiveDemoTest.java | 148 --------- .../genai/anthropic/OciAnthropicTest.java | 82 ----- .../pom.xml | 51 +-- .../pom.xml | 12 +- .../com/oracle/genai/auth/OciAuthConfig.java | 102 ++++++ .../oracle/genai}/auth/OciAuthException.java | 2 +- .../genai}/auth/OciAuthProviderFactory.java | 4 +- .../genai/auth}/OciEndpointResolver.java | 24 +- .../genai/auth}/OciHeaderInterceptor.java | 2 +- .../genai/auth/OciOkHttpClientFactory.java | 40 ++- .../genai/auth}/OciSigningInterceptor.java | 2 +- .../auth/OciAuthProviderFactoryTest.java | 4 +- .../genai/auth/OciEndpointResolverTest.java | 83 +++++ .../genai/auth}/OciHeaderInterceptorTest.java | 2 +- .../auth/OciOkHttpClientFactoryTest.java | 118 +++++++ .../genai/auth/OciSigningInterceptorTest.java | 133 ++++++++ .../endpoint/OciEndpointResolverTest.java | 89 ----- oci-genai-openai/pom.xml | 42 --- .../oracle/genai/openai/AsyncOciOpenAI.java | 159 --------- .../com/oracle/genai/openai/OciOpenAI.java | 278 ---------------- .../genai/openai/OciSigningHttpClient.java | 201 ----------- .../com/oracle/genai/openai/LiveDemoTest.java | 120 ------- .../oracle/genai/openai/OciOpenAITest.java | 84 ----- .../genai/openai/OpenAIIntegrationTest.java | 80 ----- pom.xml | 26 +- 34 files changed, 871 insertions(+), 2324 deletions(-) create mode 100644 examples/anthropic/OciAnthropicExample.java create mode 100644 examples/gemini-direct-http/OciGeminiDirectExample.java create mode 100644 examples/openai/OciOpenAIExample.java delete mode 100644 oci-genai-anthropic/pom.xml delete mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java delete mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java delete mode 100644 oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java delete mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java delete mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java delete mode 100644 oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java rename {oci-genai-bom => oci-genai-auth-java-bom}/pom.xml (52%) rename {oci-genai-core => oci-genai-auth-java-core}/pom.xml (79%) create mode 100644 oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java rename {oci-genai-core/src/main/java/com/oracle/genai/core => oci-genai-auth-java-core/src/main/java/com/oracle/genai}/auth/OciAuthException.java (93%) rename {oci-genai-core/src/main/java/com/oracle/genai/core => oci-genai-auth-java-core/src/main/java/com/oracle/genai}/auth/OciAuthProviderFactory.java (98%) rename {oci-genai-core/src/main/java/com/oracle/genai/core/endpoint => oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth}/OciEndpointResolver.java (70%) rename {oci-genai-core/src/main/java/com/oracle/genai/core/interceptor => oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth}/OciHeaderInterceptor.java (98%) rename oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java => oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java (68%) rename {oci-genai-core/src/main/java/com/oracle/genai/core/interceptor => oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth}/OciSigningInterceptor.java (99%) rename {oci-genai-core/src/test/java/com/oracle/genai/core => oci-genai-auth-java-core/src/test/java/com/oracle/genai}/auth/OciAuthProviderFactoryTest.java (89%) create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java rename {oci-genai-core/src/test/java/com/oracle/genai/core/interceptor => oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth}/OciHeaderInterceptorTest.java (98%) create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java delete mode 100644 oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java delete mode 100644 oci-genai-openai/pom.xml delete mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java delete mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java delete mode 100644 oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java delete mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java delete mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java delete mode 100644 oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java diff --git a/README.md b/README.md index e50aa97..9a76a59 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,33 @@ -# OCI GenAI Unified Java SDK +# OCI GenAI Auth for Java -Unified Java SDK family for integrating third-party Generative AI providers (OpenAI, Anthropic) with Oracle Cloud Infrastructure authentication and routing. +Vendor-neutral OCI authentication and request signing library for Java. Provides an OCI-signed `OkHttpClient` that you can plug into **any** vendor SDK or use directly with raw HTTP. -## Table of Contents +## What This Library Does -- [Architecture](#architecture) -- [Installation](#installation) -- [Quick Start](#quick-start) - - [OpenAI](#openai) - - [Anthropic](#anthropic) -- [Authentication](#authentication) -- [Client Configuration](#client-configuration) -- [Async Clients](#async-clients) -- [Base URL and Endpoint Overrides](#base-url-and-endpoint-overrides) -- [Error Handling](#error-handling) -- [Cleanup](#cleanup) -- [Module Reference](#module-reference) -- [Building from Source](#building-from-source) -- [License](#license) +- **OCI IAM request signing** — RSA-SHA256 signatures on every request, including body digest for POST/PUT +- **Auth provider factory** — supports `oci_config`, `security_token`, `instance_principal`, and `resource_principal` +- **Header injection** — automatically adds `CompartmentId` and custom headers +- **Endpoint resolution** — derives OCI GenAI service URLs from region codes +- **Token refresh** — handled automatically by the underlying OCI Java SDK auth providers -## Architecture +## What This Library Does NOT Do -This SDK follows the **core + provider modules + BOM** pattern used by AWS SDK v2, Azure SDK for Java, Google Cloud Java, and OCI's own existing SDK. Users import only the provider modules they need — no forced dependency bloat. +- Does **not** generate provider request/response models (no OpenAPI/codegen) +- Does **not** wrap or re-export any vendor SDK (OpenAI, Anthropic, Gemini, etc.) +- Does **not** include provider-specific shim classes -``` -oci-genai-bom Version management only (BOM) -oci-genai-core OCI IAM auth, request signing, header injection, endpoint resolution -oci-genai-openai Wraps openai-java SDK with OCI signing -oci-genai-anthropic Wraps anthropic-sdk-java with OCI signing -``` - -All modules share `oci-genai-core` for OCI authentication — signing logic is implemented once and applied consistently across all providers. +This is an **auth utilities library**. Vendor SDK integration is shown in the [examples/](examples/) directory. ## Installation -The SDK requires **Java 17+**. Add the BOM and the provider modules you need: +Requires **Java 17+** and **Maven 3.8+**. ```xml com.oracle.genai - oci-genai-bom + oci-genai-auth-java-bom 0.1.0-SNAPSHOT pom import @@ -50,92 +36,47 @@ The SDK requires **Java 17+**. Add the BOM and the provider modules you need: - com.oracle.genai - oci-genai-openai - - - - - com.oracle.genai - oci-genai-anthropic + oci-genai-auth-java-core ``` -Import only the providers you use. Each module brings in only its own dependencies. - ## Quick Start -### OpenAI +### Using OciAuthConfig (recommended) ```java -import com.openai.client.OpenAIClient; -import com.openai.models.responses.Response; -import com.openai.models.responses.ResponseCreateParams; -import com.oracle.genai.openai.OciOpenAI; - -public class OpenAIQuickStart { - public static void main(String[] args) { - OpenAIClient client = OciOpenAI.builder() - .compartmentId("") - .authType("security_token") - .profile("DEFAULT") - .region("us-chicago-1") - .build(); - - try { - Response response = client.responses().create(ResponseCreateParams.builder() - .model("openai.gpt-4o") - .store(false) - .input("Write a short poem about cloud computing.") - .build()); - - System.out.println(response.output()); - } finally { - client.close(); - } - } -} +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; +import okhttp3.OkHttpClient; + +OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId("ocid1.compartment.oc1..xxx") + .build(); + +OkHttpClient client = OciOkHttpClientFactory.build(config); +// Use this client with any vendor SDK that accepts an OkHttpClient, +// or make direct HTTP calls — every request is signed automatically. ``` -### Anthropic +### Direct factory method ```java -import com.anthropic.client.AnthropicClient; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import com.anthropic.models.messages.Model; -import com.oracle.genai.anthropic.OciAnthropic; - -public class AnthropicQuickStart { - public static void main(String[] args) { - AnthropicClient client = OciAnthropic.builder() - .compartmentId("") - .authType("security_token") - .profile("DEFAULT") - .region("us-chicago-1") - .build(); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model(Model.CLAUDE_SONNET_4_20250514) - .addUserMessage("Hello from OCI!") - .maxTokens(1024) - .build()); - - System.out.println(message.content()); - } finally { - client.close(); - } - } -} -``` +import com.oracle.genai.auth.OciAuthProviderFactory; +import com.oracle.genai.auth.OciOkHttpClientFactory; +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; + +BasicAuthenticationDetailsProvider authProvider = + OciAuthProviderFactory.create("security_token", "DEFAULT"); -## Authentication +OkHttpClient client = OciOkHttpClientFactory.create(authProvider, "ocid1.compartment.oc1..xxx"); +``` -Both `OciOpenAI` and `OciAnthropic` support all four OCI IAM authentication types through the `authType` parameter: +## Authentication Types | Auth Type | Use Case | |-----------|----------| @@ -145,171 +86,98 @@ Both `OciOpenAI` and `OciAnthropic` support all four OCI IAM authentication type | `resource_principal` | OCI Functions, Container Instances | ```java -// 1) User principal (API key) -OpenAIClient client = OciOpenAI.builder() - .authType("oci_config") - .profile("DEFAULT") - .compartmentId("") - .region("us-chicago-1") - .build(); - -// 2) Session token (local dev) -OpenAIClient client = OciOpenAI.builder() +// Session token (local dev) +OciAuthConfig config = OciAuthConfig.builder() .authType("security_token") .profile("DEFAULT") .compartmentId("") - .region("us-chicago-1") .build(); -// 3) Instance principal (OCI Compute) -OpenAIClient client = OciOpenAI.builder() +// Instance principal (OCI Compute) +OciAuthConfig config = OciAuthConfig.builder() .authType("instance_principal") .compartmentId("") - .region("us-chicago-1") - .build(); - -// 4) Resource principal (OCI Functions) -OpenAIClient client = OciOpenAI.builder() - .authType("resource_principal") - .compartmentId("") - .region("us-chicago-1") - .build(); - -// 5) Custom auth provider -BasicAuthenticationDetailsProvider authProvider = /* your provider */; -OpenAIClient client = OciOpenAI.builder() - .authProvider(authProvider) - .compartmentId("") - .region("us-chicago-1") .build(); ``` -The same `authType` and `authProvider` parameters work identically for `OciAnthropic`. - -## Client Configuration - -| Parameter | Description | Required | -|-----------|-------------|----------| -| `compartmentId` | OCI compartment OCID | Yes (for GenAI endpoints) | -| `authType` or `authProvider` | Authentication mechanism | Yes | -| `region` | OCI region code (e.g., `us-chicago-1`) | Yes (unless `baseUrl` or `serviceEndpoint` is set) | -| `baseUrl` | Fully qualified endpoint override | No | -| `serviceEndpoint` | Service endpoint without API path | No | -| `conversationStoreId` | Conversation Store OCID (OpenAI only) | No | -| `timeout` | Request timeout (default: 2 minutes) | No | -| `logRequestsAndResponses` | Debug logging of HTTP bodies | No | -| `profile` | OCI config profile name (default: `DEFAULT`) | No | - -## Async Clients +## Endpoint Resolution -Both providers include async client builders that return `CompletableFuture`-based clients: +Use `OciEndpointResolver` to derive service URLs from region codes: ```java -import com.oracle.genai.openai.AsyncOciOpenAI; -import com.oracle.genai.anthropic.AsyncOciAnthropic; +import com.oracle.genai.auth.OciEndpointResolver; -// Async OpenAI -OpenAIClientAsync openaiAsync = AsyncOciOpenAI.builder() - .compartmentId("") - .authType("security_token") - .region("us-chicago-1") - .build(); +// From region — most common +String url = OciEndpointResolver.resolveBaseUrl("us-chicago-1", null, null, "/20231130/actions/chat"); +// → https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat -openaiAsync.responses().create(params) - .thenAccept(response -> System.out.println(response.output())); - -// Async Anthropic -AnthropicClientAsync anthropicAsync = AsyncOciAnthropic.builder() - .compartmentId("") - .authType("security_token") - .region("us-chicago-1") - .build(); - -anthropicAsync.messages().create(params) - .thenAccept(message -> System.out.println(message.content())); -``` - -## Base URL and Endpoint Overrides - -Endpoint resolution priority (highest to lowest): `baseUrl` > `serviceEndpoint` > `region`. - -```java -// From region (most common) -OciOpenAI.builder() - .region("us-chicago-1") - // resolves to: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1 - ... - -// From service endpoint (SDK appends API path) -OciOpenAI.builder() - .serviceEndpoint("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com") - // resolves to: https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/openai/v1 - ... +// From service endpoint (API path appended) +String url = OciEndpointResolver.resolveBaseUrl(null, + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", + null, "/20231130/actions/chat"); // From explicit base URL (used as-is) -OciOpenAI.builder() - .baseUrl("https://custom-endpoint.example.com/v1") - ... +String url = OciEndpointResolver.resolveBaseUrl(null, null, + "https://custom-endpoint.example.com/v1", null); ``` -## Error Handling +Resolution priority: `baseUrl` > `serviceEndpoint` > `region`. -The underlying provider SDK exceptions still apply. Catch provider-specific exceptions for error handling: +## Configuration -```java -// OpenAI -try { - Response response = client.responses().create(params); -} catch (com.openai.errors.NotFoundException | com.openai.errors.UnauthorizedException e) { - System.err.println("Error: " + e.getMessage()); -} - -// Anthropic -try { - Message message = client.messages().create(params); -} catch (com.anthropic.errors.NotFoundException | com.anthropic.errors.UnauthorizedException e) { - System.err.println("Error: " + e.getMessage()); -} -``` +| Parameter | Description | Required | +|-----------|-------------|----------| +| `authType` | OCI authentication type (see table above) | Yes | +| `profile` | OCI config profile name (default: `DEFAULT`) | No | +| `compartmentId` | OCI compartment OCID | Yes (for GenAI endpoints) | +| `region` | OCI region code (e.g., `us-chicago-1`) | No (for endpoint resolution) | +| `baseUrl` | Fully qualified endpoint override | No | +| `timeout` | Request timeout (default: 2 minutes) | No | -## Cleanup +## Examples -Both client types implement `AutoCloseable`. Close them when finished to release HTTP resources: +The [examples/](examples/) directory contains standalone Java files showing how to use the OCI-signed `OkHttpClient` with different vendor SDKs: -```java -try (OpenAIClient client = OciOpenAI.builder() - .compartmentId("") - .authType("security_token") - .region("us-chicago-1") - .build()) { - // use client -} -``` +| Example | Description | +|---------|-------------| +| [examples/anthropic/](examples/anthropic/) | Anthropic Claude via the `anthropic-java` SDK | +| [examples/openai/](examples/openai/) | OpenAI-compatible models via the `openai-java` SDK | +| [examples/gemini-direct-http/](examples/gemini-direct-http/) | Google Gemini via direct OkHttp POST (no vendor SDK) | + +These examples are **not** compiled as part of the Maven build. Copy them into your own project. ## Module Reference | Module | Artifact | Responsibility | |--------|----------|----------------| -| `oci-genai-bom` | `com.oracle.genai:oci-genai-bom` | Pins all module and transitive dependency versions | -| `oci-genai-core` | `com.oracle.genai:oci-genai-core` | OCI IAM auth providers, per-request signing, header injection, endpoint resolution | -| `oci-genai-openai` | `com.oracle.genai:oci-genai-openai` | Wraps `openai-java` with OCI signing via custom `HttpClient` | -| `oci-genai-anthropic` | `com.oracle.genai:oci-genai-anthropic` | Wraps `anthropic-sdk-java` with OCI signing via custom `HttpClient` | +| `oci-genai-auth-java-bom` | `com.oracle.genai:oci-genai-auth-java-bom` | Pins dependency versions | +| `oci-genai-auth-java-core` | `com.oracle.genai:oci-genai-auth-java-core` | OCI IAM auth, request signing, header injection, endpoint resolution | ## Building from Source -Requires Java 17+ and Maven 3.8+. - ```bash -# Compile all modules +# Compile mvn clean compile -# Run tests +# Run tests (27 tests) mvn test +# Full verification +mvn clean verify + # Install to local Maven repository mvn install -DskipTests + +# Confirm no vendor SDK dependencies +mvn dependency:tree -pl oci-genai-auth-java-core ``` +## Design Notes + +- **Token refresh** is handled by OCI Java SDK auth providers (`SessionTokenAuthenticationDetailsProvider`, etc.) — no custom refresh logic needed. +- **Spec/codegen** is a separate follow-up track. This library provides auth utilities only. +- **Gemini example** uses direct HTTP because the Google Gemini Java SDK does not currently support transport injection. + ## License Copyright (c) 2025 Oracle and/or its affiliates. diff --git a/examples/anthropic/OciAnthropicExample.java b/examples/anthropic/OciAnthropicExample.java new file mode 100644 index 0000000..73bfca3 --- /dev/null +++ b/examples/anthropic/OciAnthropicExample.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +/** + * Example: Using the Anthropic Java SDK with OCI GenAI via oci-genai-auth-java-core. + * + *

    This file is a standalone example — it is NOT compiled as part of the Maven build. + * Copy it into your own project and add the required dependencies (see below). + * + *

    Dependencies (Maven)

    + *
    {@code
    + * 
    + *     com.oracle.genai
    + *     oci-genai-auth-java-core
    + *     0.1.0-SNAPSHOT
    + * 
    + * 
    + *     com.anthropic
    + *     anthropic-java
    + *     1.2.0
    + * 
    + * }
    + * + *

    Run

    + *
    + * javac -cp "lib/*" OciAnthropicExample.java
    + * java  -cp "lib/*:." OciAnthropicExample
    + * 
    + */ + +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.Model; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciEndpointResolver; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class OciAnthropicExample { + + // ── Configuration ────────────────────────────────────────────────── + private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal + private static final String PROFILE = "DEFAULT"; + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; + private static final String API_PATH = "/20231130/actions/chat"; + // ──────────────────────────────────────────────────────────────────── + + public static void main(String[] args) { + // 1. Build an OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType(AUTH_TYPE) + .profile(PROFILE) + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Resolve the OCI GenAI endpoint for Anthropic + String baseUrl = OciEndpointResolver.resolveBaseUrl( + REGION, null, null, API_PATH); + + // 3. Plug the OCI-signed client into the Anthropic SDK + // The Anthropic SDK expects an HTTP transport — we provide the signed OkHttpClient. + AnthropicClient anthropicClient = AnthropicOkHttpClient.builder() + .baseUrl(baseUrl) + .okHttpClient(ociHttpClient) + .apiKey("OCI_AUTH") // placeholder; OCI signing replaces API key auth + .build(); + + // 4. Send a chat completion request + Message message = anthropicClient.messages().create( + MessageCreateParams.builder() + .model(Model.CLAUDE_HAIKU_4_5_20251001) + .maxTokens(256) + .addUserMessage("What is the capital of France? Answer in one sentence.") + .build()); + + // 5. Print the response + for (ContentBlock block : message.content()) { + block.text().ifPresent(textBlock -> + System.out.println("Response: " + textBlock.text())); + } + } +} diff --git a/examples/gemini-direct-http/OciGeminiDirectExample.java b/examples/gemini-direct-http/OciGeminiDirectExample.java new file mode 100644 index 0000000..6f46914 --- /dev/null +++ b/examples/gemini-direct-http/OciGeminiDirectExample.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +/** + * Example: Calling Google Gemini via OCI GenAI using direct HTTP (no vendor SDK). + * + *

    This demonstrates using oci-genai-auth-java-core with raw OkHttp when a vendor + * SDK does not support transport injection. The OCI-signed OkHttpClient handles + * authentication transparently — you just build and send requests. + * + *

    This file is a standalone example — it is NOT compiled as part of the Maven build. + * Copy it into your own project and add the required dependencies (see below). + * + *

    Dependencies (Maven)

    + *
    {@code
    + * 
    + *     com.oracle.genai
    + *     oci-genai-auth-java-core
    + *     0.1.0-SNAPSHOT
    + * 
    + * }
    + * + *

    Run

    + *
    + * javac -cp "lib/*" OciGeminiDirectExample.java
    + * java  -cp "lib/*:." OciGeminiDirectExample
    + * 
    + */ + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciEndpointResolver; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import java.io.IOException; + +public class OciGeminiDirectExample { + + // ── Configuration ────────────────────────────────────────────────── + private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal + private static final String PROFILE = "DEFAULT"; + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; + private static final String API_PATH = "/20231130/actions/chat"; + // ──────────────────────────────────────────────────────────────────── + + private static final MediaType JSON = MediaType.parse("application/json"); + + public static void main(String[] args) throws IOException { + // 1. Build an OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType(AUTH_TYPE) + .profile(PROFILE) + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Resolve the OCI GenAI endpoint + String baseUrl = OciEndpointResolver.resolveBaseUrl( + REGION, null, null, API_PATH); + + // 3. Build the request JSON manually (Gemini/OpenAI-compatible format) + String requestJson = """ + { + "model": "google.gemini-2.0-flash-001", + "messages": [ + { + "role": "user", + "content": "What is the capital of France? Answer in one sentence." + } + ], + "max_tokens": 256 + } + """; + + // 4. Send the request using the OCI-signed OkHttpClient + Request request = new Request.Builder() + .url(baseUrl) + .post(RequestBody.create(requestJson, JSON)) + .build(); + + try (Response response = ociHttpClient.newCall(request).execute()) { + System.out.println("Status: " + response.code()); + if (response.body() != null) { + System.out.println("Response: " + response.body().string()); + } + } + } +} diff --git a/examples/openai/OciOpenAIExample.java b/examples/openai/OciOpenAIExample.java new file mode 100644 index 0000000..8fc09ff --- /dev/null +++ b/examples/openai/OciOpenAIExample.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +/** + * Example: Using the OpenAI Java SDK with OCI GenAI via oci-genai-auth-java-core. + * + *

    This file is a standalone example — it is NOT compiled as part of the Maven build. + * Copy it into your own project and add the required dependencies (see below). + * + *

    Dependencies (Maven)

    + *
    {@code
    + * 
    + *     com.oracle.genai
    + *     oci-genai-auth-java-core
    + *     0.1.0-SNAPSHOT
    + * 
    + * 
    + *     com.openai
    + *     openai-java
    + *     0.34.1
    + * 
    + * }
    + * + *

    Run

    + *
    + * javac -cp "lib/*" OciOpenAIExample.java
    + * java  -cp "lib/*:." OciOpenAIExample
    + * 
    + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciEndpointResolver; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class OciOpenAIExample { + + // ── Configuration ────────────────────────────────────────────────── + private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal + private static final String PROFILE = "DEFAULT"; + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; + private static final String API_PATH = "/20231130/actions/chat"; + // ──────────────────────────────────────────────────────────────────── + + public static void main(String[] args) { + // 1. Build an OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType(AUTH_TYPE) + .profile(PROFILE) + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Resolve the OCI GenAI endpoint for OpenAI-compatible API + String baseUrl = OciEndpointResolver.resolveBaseUrl( + REGION, null, null, API_PATH); + + // 3. Plug the OCI-signed client into the OpenAI SDK + OpenAIClient openAIClient = OpenAIOkHttpClient.builder() + .baseUrl(baseUrl) + .okHttpClient(ociHttpClient) + .apiKey("OCI_AUTH") // placeholder; OCI signing replaces API key auth + .build(); + + // 4. Send a chat completion request + ChatCompletion completion = openAIClient.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("meta.llama-3.1-405b-instruct") + .addUserMessage("What is the capital of France? Answer in one sentence.") + .build()); + + // 5. Print the response + completion.choices().forEach(choice -> + choice.message().content().ifPresent(content -> + System.out.println("Response: " + content))); + } +} diff --git a/oci-genai-anthropic/pom.xml b/oci-genai-anthropic/pom.xml deleted file mode 100644 index 5aa793e..0000000 --- a/oci-genai-anthropic/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - 4.0.0 - - - com.oracle.genai - oci-genai-parent - 0.1.0-SNAPSHOT - - - oci-genai-anthropic - jar - - OCI GenAI SDK :: Anthropic - - Anthropic provider module for the OCI GenAI SDK. - Wraps the official anthropic-sdk-java with OCI IAM authentication - via OkHttp interceptor. Omits X-Api-Key when calling OCI endpoints. - - - - - - com.oracle.genai - oci-genai-core - - - - - com.anthropic - anthropic-java - - - diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java deleted file mode 100644 index 7bc1b36..0000000 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/AsyncOciAnthropic.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import com.anthropic.client.AnthropicClientAsync; -import com.anthropic.client.AnthropicClientAsyncImpl; -import com.anthropic.client.okhttp.AnthropicOkHttpClientAsync; -import com.anthropic.core.ClientOptions; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.genai.core.OciHttpClientFactory; -import com.oracle.genai.core.auth.OciAuthProviderFactory; -import com.oracle.genai.core.endpoint.OciEndpointResolver; - -import java.time.Duration; - -/** - * Async OCI-authenticated Anthropic client builder. - * - *

    Creates an {@link AnthropicClientAsync} that routes requests through OCI - * Generative AI endpoints with OCI IAM request signing. - * - *

    Quick Start

    - *
    {@code
    - * AnthropicClientAsync client = AsyncOciAnthropic.builder()
    - *         .compartmentId("")
    - *         .authType("security_token")
    - *         .region("us-chicago-1")
    - *         .build();
    - *
    - * client.messages().create(MessageCreateParams.builder()
    - *         .model("anthropic.claude-3-sonnet")
    - *         .addUserMessage("Hello from OCI!")
    - *         .maxTokens(1024)
    - *         .build())
    - *     .thenAccept(message -> System.out.println(message.content()));
    - * }
    - */ -public final class AsyncOciAnthropic { - - private AsyncOciAnthropic() { - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String authType; - private String profile; - private String apiKey; - private BasicAuthenticationDetailsProvider authProvider; - private String compartmentId; - private String region; - private String serviceEndpoint; - private String baseUrl; - private Duration timeout; - private boolean logRequestsAndResponses; - - private Builder() { - } - - public Builder authType(String authType) { this.authType = authType; return this; } - public Builder profile(String profile) { this.profile = profile; return this; } - public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } - public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } - public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } - public Builder region(String region) { this.region = region; return this; } - public Builder serviceEndpoint(String serviceEndpoint) { this.serviceEndpoint = serviceEndpoint; return this; } - public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } - public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } - public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } - - public AnthropicClientAsync build() { - if (isApiKeyMode()) { - return buildApiKeyClient(); - } - return buildOciSignedClient(); - } - - private boolean isApiKeyMode() { - return (apiKey != null && !apiKey.isBlank()) - || "api_key".equals(authType); - } - - private AnthropicClientAsync buildApiKeyClient() { - String resolvedApiKey = apiKey; - if (resolvedApiKey == null || resolvedApiKey.isBlank()) { - throw new IllegalArgumentException( - "apiKey is required when authType is 'api_key'."); - } - - String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( - region, serviceEndpoint, baseUrl); - - AnthropicOkHttpClientAsync.Builder builder = AnthropicOkHttpClientAsync.builder() - .apiKey(resolvedApiKey) - .baseUrl(resolvedBaseUrl); - - if (timeout != null) { - builder.timeout(timeout); - } - - return builder.build(); - } - - private AnthropicClientAsync buildOciSignedClient() { - BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - - String resolvedBaseUrl = OciEndpointResolver.resolveAnthropicBaseUrl( - region, serviceEndpoint, baseUrl); - - if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { - throw new IllegalArgumentException( - "compartmentId is required to access the OCI Generative AI Service."); - } - - okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( - resolvedAuth, compartmentId, null, timeout, logRequestsAndResponses); - - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); - - ClientOptions clientOptions = ClientOptions.builder() - .httpClient(signingHttpClient) - .baseUrl(resolvedBaseUrl) - .putHeader("anthropic-version", "2023-06-01") - .build(); - - return new AnthropicClientAsyncImpl(clientOptions); - } - - private BasicAuthenticationDetailsProvider resolveAuthProvider() { - if (authProvider != null) return authProvider; - if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException("Either authType, authProvider, or apiKey must be provided."); - } - return OciAuthProviderFactory.create(authType, profile); - } - } -} diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java deleted file mode 100644 index 7e031a1..0000000 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciAnthropic.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.client.AnthropicClientImpl; -import com.anthropic.client.okhttp.AnthropicOkHttpClient; -import com.anthropic.core.ClientOptions; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.genai.core.OciHttpClientFactory; -import com.oracle.genai.core.auth.OciAuthProviderFactory; -import com.oracle.genai.core.endpoint.OciEndpointResolver; - -import java.time.Duration; - -/** - * OCI-authenticated Anthropic client builder. - * - *

    Creates an {@link AnthropicClient} that routes requests through OCI Generative AI - * endpoints with OCI IAM request signing. The underlying Anthropic Java SDK is used - * for all API operations — users get the full Anthropic API surface (messages, - * streaming, tool use) with OCI auth handled transparently. - * - *

    Quick Start

    - *
    {@code
    - * AnthropicClient client = OciAnthropic.builder()
    - *         .compartmentId("")
    - *         .authType("security_token")
    - *         .region("us-chicago-1")
    - *         .build();
    - *
    - * Message message = client.messages().create(MessageCreateParams.builder()
    - *         .model("anthropic.claude-3-sonnet")
    - *         .addUserMessage("Hello from OCI!")
    - *         .maxTokens(1024)
    - *         .build());
    - * }
    - * - *

    Authentication

    - *

    Supports all OCI IAM auth types via {@code authType}: - *

      - *
    • {@code oci_config} — user principal from {@code ~/.oci/config}
    • - *
    • {@code security_token} — session token from OCI CLI
    • - *
    • {@code instance_principal} — OCI Compute instances
    • - *
    • {@code resource_principal} — OCI Functions, Container Instances
    • - *
    • {@code api_key} — direct API key authentication (no OCI signing)
    • - *
    - *

    Alternatively, pass a pre-built {@link BasicAuthenticationDetailsProvider} - * via {@code authProvider()}, or use {@code apiKey()} for direct API key auth. - */ -public final class OciAnthropic { - - private OciAnthropic() { - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String authType; - private String profile; - private String apiKey; - private BasicAuthenticationDetailsProvider authProvider; - private String compartmentId; - private String region; - private String serviceEndpoint; - private String baseUrl; - private Duration timeout; - private boolean logRequestsAndResponses; - - private Builder() { - } - - /** - * Sets the OCI authentication type. - * One of: {@code oci_config}, {@code security_token}, - * {@code instance_principal}, {@code resource_principal}, - * {@code api_key}. - */ - public Builder authType(String authType) { - this.authType = authType; - return this; - } - - /** - * Sets the OCI config profile name. Used with {@code oci_config} and - * {@code security_token} auth types. Defaults to {@code "DEFAULT"}. - */ - public Builder profile(String profile) { - this.profile = profile; - return this; - } - - /** - * Sets the API key for direct authentication (no OCI signing). - * When set, requests are authenticated with this key via the - * {@code X-Api-Key} header, bypassing OCI IAM signing entirely. - */ - public Builder apiKey(String apiKey) { - this.apiKey = apiKey; - return this; - } - - /** - * Sets a pre-built OCI authentication provider. - * When set, {@code authType} and {@code profile} are ignored. - */ - public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { - this.authProvider = authProvider; - return this; - } - - /** - * Sets the OCI compartment OCID. Required for OCI Generative AI endpoints. - */ - public Builder compartmentId(String compartmentId) { - this.compartmentId = compartmentId; - return this; - } - - /** - * Sets the OCI region code (e.g., {@code "us-chicago-1"}). - */ - public Builder region(String region) { - this.region = region; - return this; - } - - /** - * Sets the OCI service endpoint (without API path). - * The Anthropic API path is appended automatically. - */ - public Builder serviceEndpoint(String serviceEndpoint) { - this.serviceEndpoint = serviceEndpoint; - return this; - } - - /** - * Sets the fully qualified base URL. Used as-is without modification. - */ - public Builder baseUrl(String baseUrl) { - this.baseUrl = baseUrl; - return this; - } - - /** Sets the request timeout. Defaults to 2 minutes. */ - public Builder timeout(Duration timeout) { - this.timeout = timeout; - return this; - } - - /** Enables debug logging of request/response bodies. */ - public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { - this.logRequestsAndResponses = logRequestsAndResponses; - return this; - } - - /** - * Builds the Anthropic client. - * - *

    When {@code apiKey} is set (or {@code authType} is {@code "api_key"}), - * creates a native Anthropic SDK client with direct API key auth. - * Otherwise, creates an OCI-authenticated client with IAM request signing. - * - * @return a configured {@link AnthropicClient} - * @throws IllegalArgumentException if required parameters are missing - */ - public AnthropicClient build() { - // API key mode: use native Anthropic SDK client directly - if (isApiKeyMode()) { - return buildApiKeyClient(); - } - - // OCI auth mode: use custom signing HTTP client - return buildOciSignedClient(); - } - - private boolean isApiKeyMode() { - return (apiKey != null && !apiKey.isBlank()) - || "api_key".equals(authType); - } - - private AnthropicClient buildApiKeyClient() { - String resolvedApiKey = apiKey; - if (resolvedApiKey == null || resolvedApiKey.isBlank()) { - throw new IllegalArgumentException( - "apiKey is required when authType is 'api_key'."); - } - - String resolvedBaseUrl = resolveBaseUrl(); - - AnthropicOkHttpClient.Builder builder = AnthropicOkHttpClient.builder() - .apiKey(resolvedApiKey) - .baseUrl(resolvedBaseUrl); - - if (timeout != null) { - builder.timeout(timeout); - } - - return builder.build(); - } - - private AnthropicClient buildOciSignedClient() { - BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - - String resolvedBaseUrl = resolveBaseUrl(); - - if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { - throw new IllegalArgumentException( - "compartmentId is required to access the OCI Generative AI Service."); - } - - okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( - resolvedAuth, compartmentId, null, timeout, logRequestsAndResponses); - - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient); - - ClientOptions clientOptions = ClientOptions.builder() - .httpClient(signingHttpClient) - .baseUrl(resolvedBaseUrl) - .putHeader("anthropic-version", "2023-06-01") - .build(); - - return new AnthropicClientImpl(clientOptions); - } - - private String resolveBaseUrl() { - return OciEndpointResolver.resolveAnthropicBaseUrl( - region, serviceEndpoint, baseUrl); - } - - private BasicAuthenticationDetailsProvider resolveAuthProvider() { - if (authProvider != null) { - return authProvider; - } - if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException( - "Either authType, authProvider, or apiKey must be provided."); - } - return OciAuthProviderFactory.create(authType, profile); - } - } -} diff --git a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java b/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java deleted file mode 100644 index 0e6a39b..0000000 --- a/oci-genai-anthropic/src/main/java/com/oracle/genai/anthropic/OciSigningHttpClient.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import com.anthropic.core.RequestOptions; -import com.anthropic.core.http.HttpClient; -import com.anthropic.core.http.HttpRequest; -import com.anthropic.core.http.HttpRequestBody; -import com.anthropic.core.http.HttpResponse; -import com.anthropic.core.http.Headers; -import okhttp3.*; -import okio.BufferedSink; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.CompletableFuture; - -/** - * An implementation of the Anthropic SDK's {@link HttpClient} interface backed by - * an OCI-signed {@link okhttp3.OkHttpClient}. - * - *

    This bridges the Anthropic SDK's HTTP abstraction with OCI request signing. - * The underlying OkHttpClient has {@code OciSigningInterceptor} and - * {@code OciHeaderInterceptor} already configured, so every request is - * automatically signed with OCI IAM credentials. - */ -class OciSigningHttpClient implements HttpClient { - - private static final Logger LOG = LoggerFactory.getLogger(OciSigningHttpClient.class); - private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); - - private final okhttp3.OkHttpClient okHttpClient; - - OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient) { - this.okHttpClient = okHttpClient; - } - - @Override - public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - try { - Response okResponse = okHttpClient.newCall(okRequest).execute(); - return new OkHttpResponseAdapter(okResponse); - } catch (IOException e) { - throw new RuntimeException("OCI request failed: " + request.url(), e); - } - } - - @Override - public CompletableFuture executeAsync( - HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - CompletableFuture future = new CompletableFuture<>(); - - okHttpClient.newCall(okRequest).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - future.completeExceptionally( - new RuntimeException("OCI async request failed: " + request.url(), e)); - } - - @Override - public void onResponse(Call call, Response response) { - future.complete(new OkHttpResponseAdapter(response)); - } - }); - - return future; - } - - @Override - public void close() { - okHttpClient.dispatcher().executorService().shutdown(); - okHttpClient.connectionPool().evictAll(); - } - - private Request toOkHttpRequest(HttpRequest request) { - String url = request.url(); - HttpUrl parsedUrl = HttpUrl.parse(url); - if (parsedUrl == null) { - throw new IllegalArgumentException("Invalid URL: " + url); - } - - HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); - - // Add query params - var queryParams = request.queryParams(); - for (String key : queryParams.keys()) { - for (String value : queryParams.values(key)) { - urlBuilder.addQueryParameter(key, value); - } - } - - // Build headers (strip X-Api-Key since OCI signing replaces it) - okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); - var headers = request.headers(); - for (String name : headers.names()) { - // Omit API key / auth headers — OCI signing handles authentication - if ("x-api-key".equalsIgnoreCase(name) || "authorization".equalsIgnoreCase(name)) { - continue; - } - for (String value : headers.values(name)) { - headersBuilder.add(name, value); - } - } - - // Build request body - RequestBody body = null; - HttpRequestBody requestBody = request.body(); - if (requestBody != null) { - body = new RequestBody() { - @Override - public MediaType contentType() { - String ct = requestBody.contentType(); - return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; - } - - @Override - public long contentLength() { - return requestBody.contentLength(); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - try (OutputStream os = sink.outputStream()) { - requestBody.writeTo(os); - } - } - }; - } - - String method = request.method().name(); - return new Request.Builder() - .url(urlBuilder.build()) - .headers(headersBuilder.build()) - .method(method, body) - .build(); - } - - /** - * Adapts an OkHttp {@link Response} to the Anthropic SDK's {@link HttpResponse} interface. - */ - private static class OkHttpResponseAdapter implements HttpResponse { - - private final Response response; - private final Headers headers; - - OkHttpResponseAdapter(Response response) { - this.response = response; - Headers.Builder builder = Headers.builder(); - for (String name : response.headers().names()) { - for (String value : response.headers(name)) { - builder.put(name, value); - } - } - this.headers = builder.build(); - } - - @Override - public int statusCode() { - return response.code(); - } - - @Override - public Headers headers() { - return headers; - } - - @Override - public InputStream body() { - ResponseBody responseBody = response.body(); - return responseBody != null ? responseBody.byteStream() : InputStream.nullInputStream(); - } - - @Override - public void close() { - response.close(); - } - } -} diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java deleted file mode 100644 index f397240..0000000 --- a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/AnthropicIntegrationTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * Integration tests for OciAnthropic against live endpoints. - * - *

    These tests are disabled by default. To run them, remove the {@code @Disabled} - * annotations and ensure you have: - *

      - *
    • A valid OCI config at {@code ~/.oci/config} with a session token (for PPE test)
    • - *
    • A valid API key (for Dev test)
    • - *
    - */ -class AnthropicIntegrationTest { - - private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; - - /** - * Test against PPE endpoint with OCI session token auth. - * - *

    Equivalent Python: - *

    -     * client = OciAnthropic(
    -     *     auth=OciSessionAuth(profile_name="DEFAULT"),
    -     *     base_url="https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic",
    -     *     compartment_id="ocid1.tenancy.oc1..aaaaaaaau...",
    -     * )
    -     * 
    - */ - @Test - @Disabled("Requires live OCI credentials and PPE endpoint access") - void testPpeEndpointWithOciAuth() { - AnthropicClient client = OciAnthropic.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .baseUrl("https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic") - .build(); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model("anthropic.claude-haiku-4-5") - .maxTokens(256) - .addUserMessage("Write a one-sentence bedtime story about a unicorn.") - .build()); - - System.out.println("PPE Response: " + message.content()); - assert !message.content().isEmpty() : "Response should not be empty"; - } finally { - client.close(); - } - } - - /** - * Test against Dev endpoint with API key auth. - * - *

    Equivalent Python: - *

    -     * client = Anthropic(
    -     *     api_key="sk-...",
    -     *     base_url="https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic",
    -     * )
    -     * 
    - */ - @Test - @Disabled("Requires valid API key and Dev endpoint access") - void testDevEndpointWithApiKey() { - AnthropicClient client = OciAnthropic.builder() - .apiKey("YOUR_API_KEY_HERE") - .baseUrl("https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic") - .build(); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model("anthropic.claude-haiku-4-5") - .maxTokens(256) - .addUserMessage("Write a one-sentence bedtime story about a unicorn.") - .build()); - - System.out.println("Dev Response: " + message.content()); - assert !message.content().isEmpty() : "Response should not be empty"; - } finally { - client.close(); - } - } -} diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java deleted file mode 100644 index c49320a..0000000 --- a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/LiveDemoTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.models.messages.ContentBlock; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * ═══════════════════════════════════════════════════════════════════ - * Phase 2 — Live Demo: Unified SDK PoC - * Anthropic via OCI GenAI - * ═══════════════════════════════════════════════════════════════════ - * - * BEFORE (what a developer does today WITHOUT this SDK): - * - * // 1. Manually create an OCI auth provider - * ConfigFileAuthenticationDetailsProvider authProvider = - * new ConfigFileAuthenticationDetailsProvider("DEFAULT"); - * - * // 2. Build a request signer for OCI IAM - * RequestSigner signer = DefaultRequestSigner.createRequestSigner( - * authProvider, SigningStrategy.STANDARD); - * - * // 3. Know the exact OCI endpoint URL per region - * String endpoint = "https://inference.generativeai.us-chicago-1" - * + ".oci.oraclecloud.com/anthropic/v1/messages"; - * - * // 4. Manually build the JSON payload - * String json = "{\"model\":\"anthropic.claude-3-sonnet\"," - * + "\"max_tokens\":1024," - * + "\"messages\":[{\"role\":\"user\"," - * + "\"content\":\"Explain OCI in one sentence\"}]}"; - * - * // 5. Build the HTTP request with compartment headers - * Request request = new Request.Builder() - * .url(endpoint) - * .addHeader("CompartmentId", compartmentId) - * .addHeader("opc-compartment-id", compartmentId) - * .addHeader("anthropic-version", "2023-06-01") - * .post(RequestBody.create(json, JSON)) - * .build(); - * - * // 6. Sign the request (compute RSA-SHA256 digest, inject Authorization header) - * Map signedHeaders = signer.signRequest( - * request.url().uri(), "POST", existingHeaders, bodyStream); - * // ... rebuild request with signed headers ... - * - * // 7. Execute and manually parse the response - * Response response = httpClient.newCall(signedRequest).execute(); - * // ... parse JSON, extract content, handle errors ... - * - * // That's ~50 lines of boilerplate BEFORE you even get to your business logic. - * - * - * AFTER (with the Unified SDK — this is ALL the developer writes): - * See the test methods below. ~10 lines total. - * ═══════════════════════════════════════════════════════════════════ - * - * To run: remove @Disabled and execute: - * mvn -pl oci-genai-anthropic test -Dtest=LiveDemoTest - */ -class LiveDemoTest { - - private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; - - private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic"; - - /** - * Demo 1 — Anthropic via OCI (session token auth, for local dev) - * - * This is the demo to run from your laptop. - * Requires: oci session authenticate (OCI CLI) - */ - @Test - // @Disabled("Remove to run live demo") - void demo_Anthropic_SessionToken() { - // ── AFTER: this is ALL the developer writes ── - AnthropicClient client = OciAnthropic.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .baseUrl(BASE_URL) - .build(); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model("anthropic.claude-haiku-4-5") - .maxTokens(256) - .addUserMessage("Explain OCI in one sentence.") - .build()); - - System.out.println("\n══════════════════════════════════════"); - System.out.println(" Anthropic via OCI GenAI — Response"); - System.out.println("══════════════════════════════════════"); - for (ContentBlock block : message.content()) { - block.text().ifPresent(textBlock -> - System.out.println(textBlock.text())); - } - System.out.println("══════════════════════════════════════\n"); - } finally { - client.close(); - } - } - - /** - * Demo 1b — Anthropic via OCI (instance principal, for OCI Compute) - * - * This is the demo to run from an OCI VM/container. - */ - @Test - @Disabled("Remove to run live demo — requires OCI Compute instance") - void demo_Anthropic_InstancePrincipal() { - // ── AFTER: same builder, just swap authType ── - AnthropicClient client = OciAnthropic.builder() - .authType("instance_principal") - .region("us-chicago-1") - .compartmentId(COMPARTMENT_ID) - .build(); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model("anthropic.claude-haiku-4-5") - .maxTokens(256) - .addUserMessage("Explain OCI in one sentence.") - .build()); - - System.out.println("\n══════════════════════════════════════"); - System.out.println(" Anthropic (Instance Principal)"); - System.out.println("══════════════════════════════════════"); - for (ContentBlock block : message.content()) { - block.text().ifPresent(textBlock -> - System.out.println(textBlock.text())); - } - System.out.println("══════════════════════════════════════\n"); - } finally { - client.close(); - } - } -} diff --git a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java b/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java deleted file mode 100644 index aa3fc9d..0000000 --- a/oci-genai-anthropic/src/test/java/com/oracle/genai/anthropic/OciAnthropicTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.anthropic; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for {@link OciAnthropic} builder validation logic. - * - *

    These tests verify builder parameter validation and mode selection - * without making any network calls. - */ -class OciAnthropicTest { - - @Test - void builder_throwsWhenNoAuthProvided() { - OciAnthropic.Builder builder = OciAnthropic.builder() - .region("us-chicago-1") - .compartmentId("ocid1.compartment.oc1..test"); - - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when neither authType, authProvider, nor apiKey is provided"); - } - - @Test - void builder_throwsWhenApiKeyAuthTypeButNoKey() { - OciAnthropic.Builder builder = OciAnthropic.builder() - .authType("api_key") - .region("us-chicago-1"); - - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when authType is 'api_key' but no apiKey is set"); - } - - @Test - void builder_apiKeyModeReturnsClient() { - var client = OciAnthropic.builder() - .apiKey("sk-test-key-12345") - .baseUrl("https://example.com/anthropic") - .build(); - - assertNotNull(client, "API key mode should return a valid client"); - client.close(); - } - - @Test - void builder_apiKeyModeWithRegionReturnsClient() { - var client = OciAnthropic.builder() - .apiKey("sk-test-key-12345") - .region("us-chicago-1") - .build(); - - assertNotNull(client, "API key mode with region should return a valid client"); - client.close(); - } - - @Test - void builder_throwsWhenNoEndpointInfo() { - OciAnthropic.Builder builder = OciAnthropic.builder() - .apiKey("sk-test-key-12345"); - - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when no endpoint information is provided"); - } - - @Test - void builder_apiKeyModeWithAuthTypeReturnsClient() { - var client = OciAnthropic.builder() - .authType("api_key") - .apiKey("sk-test-key-12345") - .baseUrl("https://example.com/anthropic") - .build(); - - assertNotNull(client, "Explicit api_key authType should return a valid client"); - client.close(); - } -} diff --git a/oci-genai-bom/pom.xml b/oci-genai-auth-java-bom/pom.xml similarity index 52% rename from oci-genai-bom/pom.xml rename to oci-genai-auth-java-bom/pom.xml index 356f8db..208dd45 100644 --- a/oci-genai-bom/pom.xml +++ b/oci-genai-auth-java-bom/pom.xml @@ -11,51 +11,35 @@ 4.0.0 com.oracle.genai - oci-genai-bom + oci-genai-auth-java-bom 0.1.0-SNAPSHOT pom - OCI GenAI SDK :: BOM + OCI GenAI Auth :: BOM - Bill of Materials (BOM) for the OCI GenAI SDK family. - Import this POM to pin all first-party module versions and key transitive + Bill of Materials (BOM) for the OCI GenAI Auth library. + Import this POM to pin the core module version and key transitive dependency versions, preventing diamond dependency conflicts. - - 0.1.0-SNAPSHOT + 0.1.0-SNAPSHOT 3.57.2 - - 0.40.0 - 2.12.0 - 4.12.0 - 2.18.2 2.0.16 - - - com.oracle.genai - oci-genai-core - ${oci-genai.version} - - - com.oracle.genai - oci-genai-openai - ${oci-genai.version} - + com.oracle.genai - oci-genai-anthropic - ${oci-genai.version} + oci-genai-auth-java-core + ${oci-genai-auth.version} @@ -65,18 +49,6 @@ ${oci-sdk.version} - - - com.openai - openai-java - ${openai-java.version} - - - com.anthropic - anthropic-java - ${anthropic-java.version} - - com.squareup.okhttp3 @@ -88,13 +60,6 @@ logging-interceptor ${okhttp.version} - - com.fasterxml.jackson - jackson-bom - ${jackson.version} - pom - import - org.slf4j slf4j-api diff --git a/oci-genai-core/pom.xml b/oci-genai-auth-java-core/pom.xml similarity index 79% rename from oci-genai-core/pom.xml rename to oci-genai-auth-java-core/pom.xml index bc9e941..903d77f 100644 --- a/oci-genai-core/pom.xml +++ b/oci-genai-auth-java-core/pom.xml @@ -12,18 +12,18 @@ com.oracle.genai - oci-genai-parent + oci-genai-auth-java-parent 0.1.0-SNAPSHOT - oci-genai-core + oci-genai-auth-java-core jar - OCI GenAI SDK :: Core + OCI GenAI Auth :: Core - Shared foundation for the OCI GenAI SDK family. - Provides OCI IAM authentication providers, per-request signing interceptor, - compartment header injection, endpoint resolution, and retry policies. + OCI request signing and authentication utilities for calling OCI-hosted + AI compatibility endpoints. Provides OCI IAM auth providers, per-request + signing interceptor, compartment header injection, and endpoint resolution. diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java new file mode 100644 index 0000000..955b292 --- /dev/null +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import java.time.Duration; + +/** + * Configuration for OCI authentication and endpoint resolution. + * + *

    Use the {@link #builder()} to create a config instance: + *

    {@code
    + * OciAuthConfig config = OciAuthConfig.builder()
    + *         .authType("security_token")
    + *         .profile("DEFAULT")
    + *         .compartmentId("ocid1.compartment.oc1..xxx")
    + *         .region("us-chicago-1")
    + *         .build();
    + * }
    + * + *

    Supported auth types: + *

      + *
    • {@code oci_config} — User principal from {@code ~/.oci/config}
    • + *
    • {@code security_token} — Session token from OCI CLI session
    • + *
    • {@code instance_principal} — For OCI Compute instances
    • + *
    • {@code resource_principal} — For OCI Functions, Container Instances, etc.
    • + *
    + */ +public final class OciAuthConfig { + + private final String authType; + private final String profile; + private final String region; + private final String baseUrl; + private final String compartmentId; + private final Duration timeout; + + private OciAuthConfig(Builder builder) { + this.authType = builder.authType; + this.profile = builder.profile; + this.region = builder.region; + this.baseUrl = builder.baseUrl; + this.compartmentId = builder.compartmentId; + this.timeout = builder.timeout; + } + + public String authType() { return authType; } + public String profile() { return profile; } + public String region() { return region; } + public String baseUrl() { return baseUrl; } + public String compartmentId() { return compartmentId; } + public Duration timeout() { return timeout; } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String authType; + private String profile; + private String region; + private String baseUrl; + private String compartmentId; + private Duration timeout; + + private Builder() {} + + /** + * Sets the OCI authentication type. + * One of: {@code oci_config}, {@code security_token}, + * {@code instance_principal}, {@code resource_principal}. + */ + public Builder authType(String authType) { this.authType = authType; return this; } + + /** + * Sets the OCI config profile name. Used with {@code oci_config} and + * {@code security_token} auth types. Defaults to {@code "DEFAULT"}. + */ + public Builder profile(String profile) { this.profile = profile; return this; } + + /** Sets the OCI region code (e.g., {@code "us-chicago-1"}). */ + public Builder region(String region) { this.region = region; return this; } + + /** Sets the fully qualified base URL (overrides region-based resolution). */ + public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } + + /** Sets the OCI compartment OCID. */ + public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } + + /** Sets the request timeout. Defaults to 2 minutes. */ + public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } + + public OciAuthConfig build() { + if (authType == null || authType.isBlank()) { + throw new IllegalArgumentException("authType is required"); + } + return new OciAuthConfig(this); + } + } +} diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java similarity index 93% rename from oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java index 26bfa52..c34ca71 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthException.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.auth; +package com.oracle.genai.auth; /** * Thrown when an OCI authentication provider cannot be created or diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java similarity index 98% rename from oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java index 6b1dca6..76b7cfb 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/auth/OciAuthProviderFactory.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.auth; +package com.oracle.genai.auth; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; @@ -11,8 +11,6 @@ import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider; -import java.io.IOException; - /** * Factory that creates OCI {@link BasicAuthenticationDetailsProvider} instances * based on the requested authentication type. diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java similarity index 70% rename from oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java index eda6cb7..9028ee9 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/endpoint/OciEndpointResolver.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.endpoint; +package com.oracle.genai.auth; /** * Resolves the OCI Generative AI service base URL from region, service endpoint, @@ -12,7 +12,7 @@ *

    Resolution priority (highest to lowest): *

      *
    1. {@code baseUrl} — fully qualified URL, used as-is
    2. - *
    3. {@code serviceEndpoint} — service root; the provider-specific API path is appended
    4. + *
    5. {@code serviceEndpoint} — service root; the caller-supplied {@code apiPath} is appended
    6. *
    7. {@code region} — auto-derives the service endpoint from the OCI region code
    8. *
    */ @@ -26,27 +26,11 @@ private OciEndpointResolver() { } /** - * Resolves the base URL for an OpenAI-compatible endpoint. - * Appends {@code /20231130/actions/v1} to the service endpoint. - */ - public static String resolveOpenAiBaseUrl(String region, String serviceEndpoint, String baseUrl) { - return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/20231130/actions/v1"); - } - - /** - * Resolves the base URL for an Anthropic-compatible endpoint. - * Appends {@code /anthropic} to the service endpoint. - */ - public static String resolveAnthropicBaseUrl(String region, String serviceEndpoint, String baseUrl) { - return resolveBaseUrl(region, serviceEndpoint, baseUrl, "/anthropic"); - } - - /** - * Resolves a base URL with a custom API path suffix. + * Resolves a base URL with a caller-supplied API path suffix. * * @param region OCI region code (e.g., "us-chicago-1") * @param serviceEndpoint service root URL (without API path) - * @param baseUrl fully qualified URL override + * @param baseUrl fully qualified URL override (used as-is when provided) * @param apiPath the API path to append to the service endpoint * @return the resolved base URL * @throws IllegalArgumentException if none of region, serviceEndpoint, or baseUrl is provided diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java similarity index 98% rename from oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java index b23709c..844e24c 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciHeaderInterceptor.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.interceptor; +package com.oracle.genai.auth; import okhttp3.Interceptor; import okhttp3.Request; diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java similarity index 68% rename from oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java index 3d9324d..b43619b 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/OciHttpClientFactory.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java @@ -3,11 +3,9 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core; +package com.oracle.genai.auth; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.genai.core.interceptor.OciHeaderInterceptor; -import com.oracle.genai.core.interceptor.OciSigningInterceptor; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; @@ -20,18 +18,44 @@ /** * Factory for creating OkHttp clients pre-configured with OCI signing and header interceptors. * - *

    Provider modules (OpenAI, Anthropic) use this factory to obtain an OkHttpClient - * that transparently handles OCI IAM authentication on every request. + *

    Use this factory to obtain an OkHttpClient that transparently handles OCI IAM + * authentication on every request. The returned client can be used directly or plugged + * into any vendor SDK that accepts a custom OkHttpClient or HTTP transport. + * + *

    Quick Start

    + *
    {@code
    + * OciAuthConfig config = OciAuthConfig.builder()
    + *         .authType("security_token")
    + *         .compartmentId("ocid1.compartment.oc1..xxx")
    + *         .build();
    + *
    + * OkHttpClient httpClient = OciOkHttpClientFactory.build(config);
    + * }
    */ -public final class OciHttpClientFactory { +public final class OciOkHttpClientFactory { - private static final Logger LOG = LoggerFactory.getLogger(OciHttpClientFactory.class); + private static final Logger LOG = LoggerFactory.getLogger(OciOkHttpClientFactory.class); private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(2); - private OciHttpClientFactory() { + private OciOkHttpClientFactory() { // utility class } + /** + * Creates an OCI-signed OkHttpClient from the given config. + * + * @param config the OCI auth configuration + * @return a configured OkHttpClient with signing and header interceptors + */ + public static OkHttpClient build(OciAuthConfig config) { + Objects.requireNonNull(config, "config must not be null"); + + BasicAuthenticationDetailsProvider authProvider = + OciAuthProviderFactory.create(config.authType(), config.profile()); + + return create(authProvider, config.compartmentId(), null, config.timeout(), false); + } + /** * Creates an OkHttpClient with OCI signing and header injection. * diff --git a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java similarity index 99% rename from oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java rename to oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java index 9410fb1..6bccaa1 100644 --- a/oci-genai-core/src/main/java/com/oracle/genai/core/interceptor/OciSigningInterceptor.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.interceptor; +package com.oracle.genai.auth; import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; import com.oracle.bmc.http.signing.DefaultRequestSigner; diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java similarity index 89% rename from oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java rename to oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java index a1699a2..6fbd39a 100644 --- a/oci-genai-core/src/test/java/com/oracle/genai/core/auth/OciAuthProviderFactoryTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.auth; +package com.oracle.genai.auth; import org.junit.jupiter.api.Test; @@ -33,8 +33,6 @@ void create_throwsOnUnsupportedAuthType() { @Test void create_ociConfigThrowsGracefullyWhenNoConfigFile() { - // When ~/.oci/config doesn't exist or profile is invalid, - // we expect an OciAuthException (not a raw IOException) assertThrows(OciAuthException.class, () -> OciAuthProviderFactory.create("oci_config", "NONEXISTENT_PROFILE_XYZ")); } diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java new file mode 100644 index 0000000..8c0fc6a --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OciEndpointResolverTest { + + @Test + void resolveBaseUrl_fromRegion() { + String url = OciEndpointResolver.resolveBaseUrl("us-chicago-1", null, null, "/v1/test"); + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1/test", + url); + } + + @Test + void resolveBaseUrl_fromServiceEndpoint() { + String url = OciEndpointResolver.resolveBaseUrl( + null, "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", null, "/v1/test"); + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1/test", + url); + } + + @Test + void resolveBaseUrl_fromBaseUrl() { + String url = OciEndpointResolver.resolveBaseUrl( + null, null, "https://custom-endpoint.example.com/v1", "/ignored"); + assertEquals("https://custom-endpoint.example.com/v1", url); + } + + @Test + void resolveBaseUrl_baseUrlTakesPrecedence() { + String url = OciEndpointResolver.resolveBaseUrl( + "us-chicago-1", + "https://service.example.com", + "https://override.example.com/v1", + "/v1/test"); + assertEquals("https://override.example.com/v1", url); + } + + @Test + void resolveBaseUrl_serviceEndpointTakesPrecedenceOverRegion() { + String url = OciEndpointResolver.resolveBaseUrl( + "us-chicago-1", + "https://custom-service.example.com", + null, + "/v1/test"); + assertEquals("https://custom-service.example.com/v1/test", url); + } + + @Test + void resolveBaseUrl_stripsTrailingSlash() { + String url = OciEndpointResolver.resolveBaseUrl( + null, "https://service.example.com/", null, "/v1/test"); + assertEquals("https://service.example.com/v1/test", url); + } + + @Test + void resolveBaseUrl_throwsWhenNothingProvided() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl(null, null, null, "/v1/test")); + } + + @Test + void buildServiceEndpoint_fromRegion() { + assertEquals( + "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", + OciEndpointResolver.buildServiceEndpoint("us-chicago-1")); + } + + @Test + void buildServiceEndpoint_throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.buildServiceEndpoint(null)); + } +} diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java similarity index 98% rename from oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java rename to oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java index f84e747..27ed1ab 100644 --- a/oci-genai-core/src/test/java/com/oracle/genai/core/interceptor/OciHeaderInterceptorTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java @@ -3,7 +3,7 @@ * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ -package com.oracle.genai.core.interceptor; +package com.oracle.genai.auth; import okhttp3.OkHttpClient; import okhttp3.Request; diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java new file mode 100644 index 0000000..46931d6 --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.time.Duration; +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class OciOkHttpClientFactoryTest { + + private MockWebServer server; + private BasicAuthenticationDetailsProvider authProvider; + + @BeforeEach + void setUp() throws Exception { + server = new MockWebServer(); + server.start(); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + String pem = "-----BEGIN PRIVATE KEY-----\n" + + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString( + keyPair.getPrivate().getEncoded()) + + "\n-----END PRIVATE KEY-----"; + + authProvider = mock(BasicAuthenticationDetailsProvider.class); + when(authProvider.getKeyId()).thenReturn( + "ocid1.tenancy.oc1..test/ocid1.user.oc1..test/aa:bb:cc:dd"); + when(authProvider.getPrivateKey()).thenAnswer( + inv -> new ByteArrayInputStream(pem.getBytes())); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + void create_installsInterceptors() { + OkHttpClient client = OciOkHttpClientFactory.create( + authProvider, "ocid1.compartment.oc1..test"); + + List interceptors = client.interceptors(); + assertTrue(interceptors.stream().anyMatch(i -> i instanceof OciHeaderInterceptor), + "Should contain OciHeaderInterceptor"); + assertTrue(interceptors.stream().anyMatch(i -> i instanceof OciSigningInterceptor), + "Should contain OciSigningInterceptor"); + } + + @Test + void create_requestGoesThoughMockServer() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("ok")); + + OkHttpClient client = OciOkHttpClientFactory.create( + authProvider, "ocid1.compartment.oc1..test"); + + var response = client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute(); + + assertEquals(200, response.code()); + response.close(); + + RecordedRequest request = server.takeRequest(); + assertEquals("ocid1.compartment.oc1..test", request.getHeader("CompartmentId")); + assertNotNull(request.getHeader("Authorization")); + } + + @Test + void create_setsCustomTimeout() { + OkHttpClient client = OciOkHttpClientFactory.create( + authProvider, null, null, Duration.ofSeconds(30), false); + + assertEquals(30_000, client.connectTimeoutMillis()); + assertEquals(30_000, client.readTimeoutMillis()); + assertEquals(30_000, client.writeTimeoutMillis()); + } + + @Test + void create_usesDefaultTimeoutWhenNull() { + OkHttpClient client = OciOkHttpClientFactory.create( + authProvider, null, null, null, false); + + assertEquals(120_000, client.connectTimeoutMillis()); + assertEquals(120_000, client.readTimeoutMillis()); + assertEquals(120_000, client.writeTimeoutMillis()); + } + + @Test + void create_throwsOnNullAuthProvider() { + assertThrows(NullPointerException.class, () -> + OciOkHttpClientFactory.create(null, null)); + } +} diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java new file mode 100644 index 0000000..8bbbe2f --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class OciSigningInterceptorTest { + + private MockWebServer server; + private OkHttpClient client; + + @BeforeEach + void setUp() throws Exception { + server = new MockWebServer(); + server.start(); + + // Generate a real RSA key pair for OCI SDK signing + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + // PEM-encode the private key (PKCS#8 format) so getPrivateKey() returns an InputStream + String pem = "-----BEGIN PRIVATE KEY-----\n" + + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString( + keyPair.getPrivate().getEncoded()) + + "\n-----END PRIVATE KEY-----"; + + // Mock the OCI auth provider with real key material + BasicAuthenticationDetailsProvider authProvider = + mock(BasicAuthenticationDetailsProvider.class); + when(authProvider.getKeyId()).thenReturn( + "ocid1.tenancy.oc1..test/ocid1.user.oc1..test/aa:bb:cc:dd"); + when(authProvider.getPrivateKey()).thenAnswer( + inv -> new ByteArrayInputStream(pem.getBytes())); + + client = new OkHttpClient.Builder() + .addInterceptor(new OciSigningInterceptor(authProvider)) + .build(); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + void addsAuthorizationHeader() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + String auth = request.getHeader("Authorization"); + assertNotNull(auth, "Authorization header should be present"); + assertTrue(auth.startsWith("Signature"), "Should be OCI signature format"); + } + + @Test + void addsDateHeader() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertNotNull(request.getHeader("date"), "date header should be present"); + } + + @Test + void signsPostBodyWithContentHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + RequestBody body = RequestBody.create( + "{\"message\":\"hello\"}", MediaType.parse("application/json")); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .post(body) + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertNotNull(request.getHeader("Authorization"), "Authorization should be present"); + assertNotNull(request.getHeader("x-content-sha256"), "x-content-sha256 should be present for POST"); + assertEquals("{\"message\":\"hello\"}", request.getBody().readUtf8(), + "Body should be preserved after signing"); + } + + @Test + void preservesOriginalHeaders() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200)); + + client.newCall(new Request.Builder() + .url(server.url("/test")) + .header("X-Custom-Header", "custom-value") + .build()).execute().close(); + + RecordedRequest request = server.takeRequest(); + assertEquals("custom-value", request.getHeader("X-Custom-Header")); + assertNotNull(request.getHeader("Authorization")); + } + + @Test + void throwsOnNullAuthProvider() { + assertThrows(NullPointerException.class, () -> + new OciSigningInterceptor(null)); + } +} diff --git a/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java b/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java deleted file mode 100644 index fcad4d5..0000000 --- a/oci-genai-core/src/test/java/com/oracle/genai/core/endpoint/OciEndpointResolverTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.core.endpoint; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class OciEndpointResolverTest { - - @Test - void resolveOpenAiBaseUrl_fromRegion() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl("us-chicago-1", null, null); - assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1", - url); - } - - @Test - void resolveOpenAiBaseUrl_fromServiceEndpoint() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl( - null, "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", null); - assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1", - url); - } - - @Test - void resolveOpenAiBaseUrl_fromBaseUrl() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl( - null, null, "https://custom-endpoint.example.com/openai/v1"); - assertEquals("https://custom-endpoint.example.com/openai/v1", url); - } - - @Test - void resolveOpenAiBaseUrl_baseUrlTakesPrecedence() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl( - "us-chicago-1", - "https://service.example.com", - "https://override.example.com/v1"); - assertEquals("https://override.example.com/v1", url); - } - - @Test - void resolveOpenAiBaseUrl_serviceEndpointTakesPrecedenceOverRegion() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl( - "us-chicago-1", - "https://custom-service.example.com", - null); - assertEquals("https://custom-service.example.com/20231130/actions/v1", url); - } - - @Test - void resolveOpenAiBaseUrl_stripsTrailingSlash() { - String url = OciEndpointResolver.resolveOpenAiBaseUrl( - null, "https://service.example.com/", null); - assertEquals("https://service.example.com/20231130/actions/v1", url); - } - - @Test - void resolveAnthropicBaseUrl_fromRegion() { - String url = OciEndpointResolver.resolveAnthropicBaseUrl("us-chicago-1", null, null); - assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic", - url); - } - - @Test - void resolveBaseUrl_throwsWhenNothingProvided() { - assertThrows(IllegalArgumentException.class, () -> - OciEndpointResolver.resolveOpenAiBaseUrl(null, null, null)); - } - - @Test - void buildServiceEndpoint_fromRegion() { - assertEquals( - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - OciEndpointResolver.buildServiceEndpoint("us-chicago-1")); - } - - @Test - void buildServiceEndpoint_throwsOnNull() { - assertThrows(IllegalArgumentException.class, () -> - OciEndpointResolver.buildServiceEndpoint(null)); - } -} diff --git a/oci-genai-openai/pom.xml b/oci-genai-openai/pom.xml deleted file mode 100644 index 2375ee1..0000000 --- a/oci-genai-openai/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - 4.0.0 - - - com.oracle.genai - oci-genai-parent - 0.1.0-SNAPSHOT - - - oci-genai-openai - jar - - OCI GenAI SDK :: OpenAI - - OpenAI provider module for the OCI GenAI SDK. - Wraps the official openai-java SDK with OCI IAM authentication - via OkHttp interceptor, targeting OCI Generative AI endpoints. - - - - - - com.oracle.genai - oci-genai-core - - - - - com.openai - openai-java - - - diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java deleted file mode 100644 index 00d2829..0000000 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/AsyncOciOpenAI.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import com.openai.client.OpenAIClientAsync; -import com.openai.client.OpenAIClientAsyncImpl; -import com.openai.client.okhttp.OpenAIOkHttpClientAsync; -import com.openai.core.ClientOptions; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.genai.core.OciHttpClientFactory; -import com.oracle.genai.core.auth.OciAuthProviderFactory; -import com.oracle.genai.core.endpoint.OciEndpointResolver; - -import java.time.Duration; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Async OCI-authenticated OpenAI client builder. - * - *

    Creates an {@link OpenAIClientAsync} that routes requests through OCI - * Generative AI endpoints with OCI IAM request signing. Supports async/await - * patterns using {@link java.util.concurrent.CompletableFuture}. - * - *

    Quick Start

    - *
    {@code
    - * OpenAIClientAsync client = AsyncOciOpenAI.builder()
    - *         .compartmentId("")
    - *         .authType("security_token")
    - *         .region("us-chicago-1")
    - *         .build();
    - *
    - * client.responses().create(ResponseCreateParams.builder()
    - *         .model("openai.gpt-4o")
    - *         .store(false)
    - *         .input("Write a short poem about cloud computing.")
    - *         .build())
    - *     .thenAccept(response -> System.out.println(response.output()));
    - * }
    - */ -public final class AsyncOciOpenAI { - - private static final String CONVERSATION_STORE_ID_HEADER = "opc-conversation-store-id"; - - private AsyncOciOpenAI() { - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String authType; - private String profile; - private String apiKey; - private BasicAuthenticationDetailsProvider authProvider; - private String compartmentId; - private String conversationStoreId; - private String region; - private String serviceEndpoint; - private String baseUrl; - private Duration timeout; - private boolean logRequestsAndResponses; - - private Builder() { - } - - public Builder authType(String authType) { this.authType = authType; return this; } - public Builder profile(String profile) { this.profile = profile; return this; } - public Builder apiKey(String apiKey) { this.apiKey = apiKey; return this; } - public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { this.authProvider = authProvider; return this; } - public Builder compartmentId(String compartmentId) { this.compartmentId = compartmentId; return this; } - public Builder conversationStoreId(String conversationStoreId) { this.conversationStoreId = conversationStoreId; return this; } - public Builder region(String region) { this.region = region; return this; } - public Builder serviceEndpoint(String serviceEndpoint) { this.serviceEndpoint = serviceEndpoint; return this; } - public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } - public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } - public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { this.logRequestsAndResponses = logRequestsAndResponses; return this; } - - public OpenAIClientAsync build() { - if (isApiKeyMode()) { - return buildApiKeyClient(); - } - return buildOciSignedClient(); - } - - private boolean isApiKeyMode() { - return (apiKey != null && !apiKey.isBlank()) - || "api_key".equals(authType); - } - - private OpenAIClientAsync buildApiKeyClient() { - String resolvedApiKey = apiKey; - if (resolvedApiKey == null || resolvedApiKey.isBlank()) { - throw new IllegalArgumentException( - "apiKey is required when authType is 'api_key'."); - } - - String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( - region, serviceEndpoint, baseUrl); - - OpenAIOkHttpClientAsync.Builder builder = OpenAIOkHttpClientAsync.builder() - .apiKey(resolvedApiKey) - .baseUrl(resolvedBaseUrl); - - if (timeout != null) { - builder.timeout(timeout); - } - - return builder.build(); - } - - private OpenAIClientAsync buildOciSignedClient() { - BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - - String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( - region, serviceEndpoint, baseUrl); - - if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { - throw new IllegalArgumentException( - "compartmentId is required to access the OCI Generative AI Service."); - } - - Map additionalHeaders = buildAdditionalHeaders(); - - okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( - resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); - - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient, resolvedBaseUrl); - - ClientOptions clientOptions = ClientOptions.builder() - .httpClient(signingHttpClient) - .baseUrl(resolvedBaseUrl) - .apiKey("OCI_AUTH_NOT_USED") - .build(); - - return new OpenAIClientAsyncImpl(clientOptions); - } - - private Map buildAdditionalHeaders() { - Map headers = new LinkedHashMap<>(); - if (conversationStoreId != null && !conversationStoreId.isBlank()) { - headers.put(CONVERSATION_STORE_ID_HEADER, conversationStoreId); - } - return headers.isEmpty() ? null : headers; - } - - private BasicAuthenticationDetailsProvider resolveAuthProvider() { - if (authProvider != null) return authProvider; - if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException("Either authType, authProvider, or apiKey must be provided."); - } - return OciAuthProviderFactory.create(authType, profile); - } - } -} diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java deleted file mode 100644 index 09d4ea7..0000000 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciOpenAI.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import com.openai.client.OpenAIClient; -import com.openai.client.OpenAIClientImpl; -import com.openai.client.okhttp.OpenAIOkHttpClient; -import com.openai.core.ClientOptions; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.genai.core.OciHttpClientFactory; -import com.oracle.genai.core.auth.OciAuthProviderFactory; -import com.oracle.genai.core.endpoint.OciEndpointResolver; - -import java.time.Duration; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * OCI-authenticated OpenAI client builder. - * - *

    Creates an {@link OpenAIClient} that routes requests through OCI Generative AI - * endpoints with OCI IAM request signing. The underlying OpenAI Java SDK is used - * for all API operations — users get the full OpenAI API surface (chat completions, - * responses, embeddings) with OCI auth handled transparently. - * - *

    Quick Start

    - *
    {@code
    - * OpenAIClient client = OciOpenAI.builder()
    - *         .compartmentId("")
    - *         .authType("security_token")
    - *         .profile("DEFAULT")
    - *         .region("us-chicago-1")
    - *         .build();
    - *
    - * Response response = client.responses().create(ResponseCreateParams.builder()
    - *         .model("openai.gpt-4o")
    - *         .store(false)
    - *         .input("Write a short poem about cloud computing.")
    - *         .build());
    - * }
    - * - *

    Authentication

    - *

    Supports all OCI IAM auth types via {@code authType}: - *

      - *
    • {@code oci_config} — user principal from {@code ~/.oci/config}
    • - *
    • {@code security_token} — session token from OCI CLI
    • - *
    • {@code instance_principal} — OCI Compute instances
    • - *
    • {@code resource_principal} — OCI Functions, Container Instances
    • - *
    - *

    Alternatively, pass a pre-built {@link BasicAuthenticationDetailsProvider} - * via {@code authProvider()}. - */ -public final class OciOpenAI { - - /** Header key for conversation store OCID. */ - private static final String CONVERSATION_STORE_ID_HEADER = "opc-conversation-store-id"; - - private OciOpenAI() { - // static builder entry point only - } - - /** - * Returns a new builder for configuring an OCI-authenticated OpenAI client. - */ - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String authType; - private String profile; - private String apiKey; - private BasicAuthenticationDetailsProvider authProvider; - private String compartmentId; - private String conversationStoreId; - private String region; - private String serviceEndpoint; - private String baseUrl; - private Duration timeout; - private boolean logRequestsAndResponses; - - private Builder() { - } - - /** - * Sets the OCI authentication type. - * One of: {@code oci_config}, {@code security_token}, - * {@code instance_principal}, {@code resource_principal}, - * {@code api_key}. - */ - public Builder authType(String authType) { - this.authType = authType; - return this; - } - - /** - * Sets the OCI config profile name. Used with {@code oci_config} and - * {@code security_token} auth types. Defaults to {@code "DEFAULT"}. - */ - public Builder profile(String profile) { - this.profile = profile; - return this; - } - - /** - * Sets the API key for direct authentication (no OCI signing). - * When set, requests are authenticated with this key via the - * {@code Authorization: Bearer} header, bypassing OCI IAM signing entirely. - */ - public Builder apiKey(String apiKey) { - this.apiKey = apiKey; - return this; - } - - /** - * Sets a pre-built OCI authentication provider. - * When set, {@code authType} and {@code profile} are ignored. - */ - public Builder authProvider(BasicAuthenticationDetailsProvider authProvider) { - this.authProvider = authProvider; - return this; - } - - /** - * Sets the OCI compartment OCID. Required for OCI Generative AI endpoints. - */ - public Builder compartmentId(String compartmentId) { - this.compartmentId = compartmentId; - return this; - } - - /** - * Sets the optional Conversation Store OCID, attached to every request - * as the {@code opc-conversation-store-id} header. - */ - public Builder conversationStoreId(String conversationStoreId) { - this.conversationStoreId = conversationStoreId; - return this; - } - - /** - * Sets the OCI region code (e.g., {@code "us-chicago-1"}). - * Auto-derives the service endpoint URL. - */ - public Builder region(String region) { - this.region = region; - return this; - } - - /** - * Sets the OCI service endpoint (without API path). - * {@code /openai/v1} is appended automatically. - * Takes precedence over {@code region}. - */ - public Builder serviceEndpoint(String serviceEndpoint) { - this.serviceEndpoint = serviceEndpoint; - return this; - } - - /** - * Sets the fully qualified base URL (including API path). - * Used as-is without modification. - * Takes precedence over {@code serviceEndpoint} and {@code region}. - */ - public Builder baseUrl(String baseUrl) { - this.baseUrl = baseUrl; - return this; - } - - /** - * Sets the request timeout. Defaults to 2 minutes. - */ - public Builder timeout(Duration timeout) { - this.timeout = timeout; - return this; - } - - /** - * Enables debug logging of request/response bodies. - */ - public Builder logRequestsAndResponses(boolean logRequestsAndResponses) { - this.logRequestsAndResponses = logRequestsAndResponses; - return this; - } - - /** - * Builds the OpenAI client. - * - *

    When {@code apiKey} is set (or {@code authType} is {@code "api_key"}), - * creates a native OpenAI SDK client with direct API key auth. - * Otherwise, creates an OCI-authenticated client with IAM request signing. - * - * @return a configured {@link OpenAIClient} - * @throws IllegalArgumentException if required parameters are missing - */ - public OpenAIClient build() { - if (isApiKeyMode()) { - return buildApiKeyClient(); - } - return buildOciSignedClient(); - } - - private boolean isApiKeyMode() { - return (apiKey != null && !apiKey.isBlank()) - || "api_key".equals(authType); - } - - private OpenAIClient buildApiKeyClient() { - String resolvedApiKey = apiKey; - if (resolvedApiKey == null || resolvedApiKey.isBlank()) { - throw new IllegalArgumentException( - "apiKey is required when authType is 'api_key'."); - } - - String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( - region, serviceEndpoint, baseUrl); - - OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder() - .apiKey(resolvedApiKey) - .baseUrl(resolvedBaseUrl); - - if (timeout != null) { - builder.timeout(timeout); - } - - return builder.build(); - } - - private OpenAIClient buildOciSignedClient() { - BasicAuthenticationDetailsProvider resolvedAuth = resolveAuthProvider(); - - String resolvedBaseUrl = OciEndpointResolver.resolveOpenAiBaseUrl( - region, serviceEndpoint, baseUrl); - - if (resolvedBaseUrl.contains("generativeai") && (compartmentId == null || compartmentId.isBlank())) { - throw new IllegalArgumentException( - "compartmentId is required to access the OCI Generative AI Service."); - } - - Map additionalHeaders = buildAdditionalHeaders(); - - okhttp3.OkHttpClient signedOkHttpClient = OciHttpClientFactory.create( - resolvedAuth, compartmentId, additionalHeaders, timeout, logRequestsAndResponses); - - OciSigningHttpClient signingHttpClient = new OciSigningHttpClient(signedOkHttpClient, resolvedBaseUrl); - - ClientOptions clientOptions = ClientOptions.builder() - .httpClient(signingHttpClient) - .baseUrl(resolvedBaseUrl) - .apiKey("OCI_AUTH_NOT_USED") - .build(); - - return new OpenAIClientImpl(clientOptions); - } - - private Map buildAdditionalHeaders() { - Map headers = new LinkedHashMap<>(); - if (conversationStoreId != null && !conversationStoreId.isBlank()) { - headers.put(CONVERSATION_STORE_ID_HEADER, conversationStoreId); - } - return headers.isEmpty() ? null : headers; - } - - private BasicAuthenticationDetailsProvider resolveAuthProvider() { - if (authProvider != null) { - return authProvider; - } - if (authType == null || authType.isBlank()) { - throw new IllegalArgumentException( - "Either authType, authProvider, or apiKey must be provided."); - } - return OciAuthProviderFactory.create(authType, profile); - } - } -} diff --git a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java b/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java deleted file mode 100644 index ca2a333..0000000 --- a/oci-genai-openai/src/main/java/com/oracle/genai/openai/OciSigningHttpClient.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import com.openai.core.RequestOptions; -import com.openai.core.http.HttpClient; -import com.openai.core.http.HttpRequest; -import com.openai.core.http.HttpRequestBody; -import com.openai.core.http.HttpResponse; -import com.openai.core.http.Headers; -import okhttp3.*; -import okio.BufferedSink; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * An implementation of the OpenAI SDK's {@link HttpClient} interface backed by - * an OCI-signed {@link okhttp3.OkHttpClient}. - * - *

    This bridges the OpenAI SDK's HTTP abstraction with OCI request signing. - * The underlying OkHttpClient has {@code OciSigningInterceptor} and - * {@code OciHeaderInterceptor} already configured, so every request is - * automatically signed with OCI IAM credentials. - */ -class OciSigningHttpClient implements HttpClient { - - private static final Logger LOG = LoggerFactory.getLogger(OciSigningHttpClient.class); - private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); - - private final okhttp3.OkHttpClient okHttpClient; - private final HttpUrl baseUrl; - - OciSigningHttpClient(okhttp3.OkHttpClient okHttpClient, String baseUrl) { - this.okHttpClient = okHttpClient; - this.baseUrl = HttpUrl.parse(baseUrl); - if (this.baseUrl == null) { - throw new IllegalArgumentException("Invalid base URL: " + baseUrl); - } - } - - @Override - public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - try { - Response okResponse = okHttpClient.newCall(okRequest).execute(); - return new OkHttpResponseAdapter(okResponse); - } catch (IOException e) { - throw new RuntimeException("OCI request failed: " + request.url(), e); - } - } - - @Override - public CompletableFuture executeAsync( - HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - CompletableFuture future = new CompletableFuture<>(); - - okHttpClient.newCall(okRequest).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - future.completeExceptionally( - new RuntimeException("OCI async request failed: " + request.url(), e)); - } - - @Override - public void onResponse(Call call, Response response) { - future.complete(new OkHttpResponseAdapter(response)); - } - }); - - return future; - } - - @Override - public void close() { - okHttpClient.dispatcher().executorService().shutdown(); - okHttpClient.connectionPool().evictAll(); - } - - private Request toOkHttpRequest(HttpRequest request) { - // Build the full URL: baseUrl + pathSegments, or use url() if available - HttpUrl.Builder urlBuilder; - String url = request.url(); - if (url != null && !url.isBlank()) { - HttpUrl parsedUrl = HttpUrl.parse(url); - if (parsedUrl == null) { - throw new IllegalArgumentException("Invalid URL: " + url); - } - urlBuilder = parsedUrl.newBuilder(); - } else { - // Build from baseUrl + pathSegments (how the OpenAI SDK works) - urlBuilder = baseUrl.newBuilder(); - List pathSegments = request.pathSegments(); - if (pathSegments != null) { - for (String segment : pathSegments) { - urlBuilder.addPathSegment(segment); - } - } - } - - // Add query params - var queryParams = request.queryParams(); - for (String key : queryParams.keys()) { - for (String value : queryParams.values(key)) { - urlBuilder.addQueryParameter(key, value); - } - } - - // Build headers - okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); - var headers = request.headers(); - for (String name : headers.names()) { - for (String value : headers.values(name)) { - headersBuilder.add(name, value); - } - } - - // Build request body - RequestBody body = null; - HttpRequestBody requestBody = request.body(); - if (requestBody != null) { - body = new RequestBody() { - @Override - public MediaType contentType() { - String ct = requestBody.contentType(); - return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; - } - - @Override - public long contentLength() { - return requestBody.contentLength(); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - try (OutputStream os = sink.outputStream()) { - requestBody.writeTo(os); - } - } - }; - } - - String method = request.method().name(); - return new Request.Builder() - .url(urlBuilder.build()) - .headers(headersBuilder.build()) - .method(method, body) - .build(); - } - - /** - * Adapts an OkHttp {@link Response} to the OpenAI SDK's {@link HttpResponse} interface. - */ - private static class OkHttpResponseAdapter implements HttpResponse { - - private final Response response; - private final Headers headers; - - OkHttpResponseAdapter(Response response) { - this.response = response; - // Convert OkHttp headers to SDK Headers - Headers.Builder builder = Headers.builder(); - for (String name : response.headers().names()) { - for (String value : response.headers(name)) { - builder.put(name, value); - } - } - this.headers = builder.build(); - } - - @Override - public int statusCode() { - return response.code(); - } - - @Override - public Headers headers() { - return headers; - } - - @Override - public InputStream body() { - ResponseBody responseBody = response.body(); - return responseBody != null ? responseBody.byteStream() : InputStream.nullInputStream(); - } - - @Override - public void close() { - response.close(); - } - } -} diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java deleted file mode 100644 index 1defe51..0000000 --- a/oci-genai-openai/src/test/java/com/oracle/genai/openai/LiveDemoTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import com.openai.client.OpenAIClient; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * ═══════════════════════════════════════════════════════════════════ - * Phase 2 — Live Demo: Unified SDK PoC - * OpenAI-compatible via OCI GenAI - * ═══════════════════════════════════════════════════════════════════ - * - * BEFORE (what a developer does today WITHOUT this SDK): - * - * // Same 50-line boilerplate as Anthropic: - * // - manual OCI auth provider setup - * // - manual request signing (RSA-SHA256) - * // - know the endpoint: .../20231130/actions/v1/chat/completions - * // - manual compartment header injection - * // - manual JSON payload construction - * // - manual response parsing - * // - * // AND the OpenAI endpoint path is DIFFERENT from Anthropic! - * // Anthropic: /anthropic/v1/messages - * // OpenAI: /20231130/actions/v1/chat/completions - * // - * // Developers have to know both. The SDK handles this automatically. - * - * - * AFTER (with the Unified SDK): - * Almost identical builder pattern — that's the point. - * See the test methods below. - * ═══════════════════════════════════════════════════════════════════ - * - * To run: remove @Disabled and execute: - * mvn -pl oci-genai-openai test -Dtest=LiveDemoTest - */ -class LiveDemoTest { - - private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; - - private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1"; - - /** - * Demo 2 — OpenAI-compatible via OCI (session token auth, for local dev) - * - * This is the demo to run from your laptop. - * Requires: oci session authenticate (OCI CLI) - */ - @Test - // @Disabled("Remove to run live demo") - void demo_OpenAI_SessionToken() { - // ── AFTER: almost identical builder — that's the point ── - OpenAIClient client = OciOpenAI.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .baseUrl(BASE_URL) - .build(); - - try { - ChatCompletion completion = client.chat().completions().create( - ChatCompletionCreateParams.builder() - .model("xai.grok-3") - .addUserMessage("Explain OCI in one sentence.") - .build()); - - System.out.println("\n══════════════════════════════════════"); - System.out.println(" OpenAI via OCI GenAI — Response"); - System.out.println("══════════════════════════════════════"); - completion.choices().forEach(choice -> - choice.message().content().ifPresent(System.out::println)); - System.out.println("══════════════════════════════════════\n"); - } finally { - client.close(); - } - } - - /** - * Demo 2b — OpenAI-compatible via OCI (instance principal, for OCI Compute) - * - * This is the demo to run from an OCI VM/container. - */ - @Test - @Disabled("Remove to run live demo — requires OCI Compute instance") - void demo_OpenAI_InstancePrincipal() { - // ── AFTER: same builder, just swap authType ── - OpenAIClient client = OciOpenAI.builder() - .authType("instance_principal") - .region("us-chicago-1") - .compartmentId(COMPARTMENT_ID) - .build(); - - try { - ChatCompletion completion = client.chat().completions().create( - ChatCompletionCreateParams.builder() - .model("xai.grok-3") - .addUserMessage("Explain OCI in one sentence.") - .build()); - - System.out.println("\n══════════════════════════════════════"); - System.out.println(" OpenAI (Instance Principal)"); - System.out.println("══════════════════════════════════════"); - completion.choices().forEach(choice -> - choice.message().content().ifPresent(System.out::println)); - System.out.println("══════════════════════════════════════\n"); - } finally { - client.close(); - } - } -} diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java deleted file mode 100644 index 4d35423..0000000 --- a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OciOpenAITest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for {@link OciOpenAI} builder validation logic. - * - *

    These tests verify builder parameter validation and mode selection - * without making any network calls. - */ -class OciOpenAITest { - - @Test - void builder_throwsWhenNoAuthProvided() { - OciOpenAI.Builder builder = OciOpenAI.builder() - .region("us-chicago-1") - .compartmentId("ocid1.compartment.oc1..test"); - - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when neither authType, authProvider, nor apiKey is provided"); - } - - @Test - void builder_throwsWhenApiKeyAuthTypeButNoKey() { - OciOpenAI.Builder builder = OciOpenAI.builder() - .authType("api_key") - .region("us-chicago-1"); - - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when authType is 'api_key' but no apiKey is set"); - } - - @Test - void builder_apiKeyModeReturnsClient() { - // API key mode should build successfully without OCI credentials - var client = OciOpenAI.builder() - .apiKey("sk-test-key-12345") - .baseUrl("https://example.com/v1") - .build(); - - assertNotNull(client, "API key mode should return a valid client"); - client.close(); - } - - @Test - void builder_apiKeyModeWithRegionReturnsClient() { - var client = OciOpenAI.builder() - .apiKey("sk-test-key-12345") - .region("us-chicago-1") - .build(); - - assertNotNull(client, "API key mode with region should return a valid client"); - client.close(); - } - - @Test - void builder_throwsWhenNoEndpointInfo() { - OciOpenAI.Builder builder = OciOpenAI.builder() - .apiKey("sk-test-key-12345"); - - // No region, serviceEndpoint, or baseUrl - assertThrows(IllegalArgumentException.class, builder::build, - "Should throw when no endpoint information is provided"); - } - - @Test - void builder_apiKeyModeWithAuthTypeReturnsClient() { - var client = OciOpenAI.builder() - .authType("api_key") - .apiKey("sk-test-key-12345") - .baseUrl("https://example.com/v1") - .build(); - - assertNotNull(client, "Explicit api_key authType should return a valid client"); - client.close(); - } -} diff --git a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java b/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java deleted file mode 100644 index af61fdf..0000000 --- a/oci-genai-openai/src/test/java/com/oracle/genai/openai/OpenAIIntegrationTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2025 Oracle and/or its affiliates. - * Licensed under the Universal Permissive License v 1.0 as shown at - * https://oss.oracle.com/licenses/upl/ - */ -package com.oracle.genai.openai; - -import com.openai.client.OpenAIClient; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * Integration tests for OciOpenAI against live endpoints. - * - *

    These tests are disabled by default. To run them, remove the {@code @Disabled} - * annotations and ensure you have: - *

      - *
    • A valid OCI config at {@code ~/.oci/config} with a session token (for PPE test)
    • - *
    • A valid API key (for Dev test)
    • - *
    - */ -class OpenAIIntegrationTest { - - private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; - - /** - * Test against PPE endpoint with OCI session token auth. - */ - @Test - @Disabled("Requires live OCI credentials and PPE endpoint access") - void testPpeEndpointWithOciAuth() { - OpenAIClient client = OciOpenAI.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .baseUrl("https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") - .build(); - - try { - ChatCompletion completion = client.chat().completions().create( - ChatCompletionCreateParams.builder() - .model("xai.grok-3") - .addUserMessage("Write a one-sentence bedtime story about a unicorn.") - .build()); - - System.out.println("PPE Response: " + completion.choices()); - assert !completion.choices().isEmpty() : "Response should not be empty"; - } finally { - client.close(); - } - } - - /** - * Test against Dev endpoint with API key auth. - */ - @Test - @Disabled("Requires valid API key and Dev endpoint access") - void testDevEndpointWithApiKey() { - OpenAIClient client = OciOpenAI.builder() - .apiKey("YOUR_API_KEY_HERE") - .baseUrl("https://dev.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") - .build(); - - try { - ChatCompletion completion = client.chat().completions().create( - ChatCompletionCreateParams.builder() - .model("xai.grok-3") - .addUserMessage("Write a one-sentence bedtime story about a unicorn.") - .build()); - - System.out.println("Dev Response: " + completion.choices()); - assert !completion.choices().isEmpty() : "Response should not be empty"; - } finally { - client.close(); - } - } -} diff --git a/pom.xml b/pom.xml index 42c282f..8e25652 100644 --- a/pom.xml +++ b/pom.xml @@ -11,16 +11,15 @@ 4.0.0 com.oracle.genai - oci-genai-parent + oci-genai-auth-java-parent 0.1.0-SNAPSHOT pom - OCI GenAI SDK :: Parent + OCI GenAI Auth :: Parent - Unified Java SDK family for integrating third-party Generative AI providers - (OpenAI, Anthropic) with Oracle Cloud Infrastructure authentication and routing. + OCI request signing and authentication utilities for calling OCI-hosted + AI compatibility endpoints using any HTTP client based on OkHttp. - https://github.com/oracle/oci-genai-java-sdk @@ -30,10 +29,8 @@ - oci-genai-bom - oci-genai-core - oci-genai-openai - oci-genai-anthropic + oci-genai-auth-java-bom + oci-genai-auth-java-core @@ -42,18 +39,13 @@ 17 - 0.1.0-SNAPSHOT + 0.1.0-SNAPSHOT 3.57.2 - - 0.40.0 - 2.12.0 - 4.12.0 - 2.18.2 2.0.16 @@ -71,8 +63,8 @@ com.oracle.genai - oci-genai-bom - ${oci-genai.version} + oci-genai-auth-java-bom + ${oci-genai-auth.version} pom import From 7bf084621bda1c22dcf9f089a14d3f06c1697578 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 26 Feb 2026 17:58:54 -0800 Subject: [PATCH 06/16] Add integration tests for Anthropic, OpenAI, and Gemini via OCI auth Validates oci-genai-auth-java-core end-to-end against PPE endpoints. Anthropic and OpenAI confirmed working. Gemini disabled pending endpoint availability. All tests @Disabled by default (require live OCI session). Co-Authored-By: Claude Opus 4.6 --- oci-genai-auth-java-core/pom.xml | 16 ++ .../genai/auth/AnthropicIntegrationTest.java | 234 +++++++++++++++++ .../genai/auth/GeminiIntegrationTest.java | 85 +++++++ .../genai/auth/OpenAIIntegrationTest.java | 239 ++++++++++++++++++ 4 files changed, 574 insertions(+) create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java diff --git a/oci-genai-auth-java-core/pom.xml b/oci-genai-auth-java-core/pom.xml index 903d77f..c87d708 100644 --- a/oci-genai-auth-java-core/pom.xml +++ b/oci-genai-auth-java-core/pom.xml @@ -50,5 +50,21 @@ org.slf4j slf4j-api + + + + com.anthropic + anthropic-java + 2.12.0 + test + + + + + com.openai + openai-java + 0.40.0 + test + diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java new file mode 100644 index 0000000..e9fab32 --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.AnthropicClientImpl; +import com.anthropic.core.ClientOptions; +import com.anthropic.core.RequestOptions; +import com.anthropic.core.http.HttpClient; +import com.anthropic.core.http.HttpRequest; +import com.anthropic.core.http.HttpRequestBody; +import com.anthropic.core.http.HttpResponse; +import com.anthropic.core.http.Headers; +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.MessageCreateParams; +import okhttp3.*; +import okio.BufferedSink; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test: OCI auth library + Anthropic Java SDK against PPE endpoint. + * + *

    Validates that oci-genai-auth-java-core produces a correctly signed + * OkHttpClient that works with the Anthropic SDK out of the box. + * + *

    To run: + *

    + * oci session authenticate          # refresh token
    + * mvn -pl oci-genai-auth-java-core test -Dtest=AnthropicIntegrationTest
    + * 
    + */ +class AnthropicIntegrationTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + private static final String BASE_URL = + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic"; + + @Test + @Disabled("Requires live OCI session — run: oci session authenticate") + void anthropic_via_oci_auth_library() { + // 1. Build OCI-signed OkHttpClient using the auth library + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Wrap in Anthropic HttpClient adapter and build client + HttpClient signingHttpClient = new AnthropicOkHttpAdapter(ociHttpClient); + + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(BASE_URL) + .putHeader("anthropic-version", "2023-06-01") + .build(); + + AnthropicClient client = new AnthropicClientImpl(clientOptions); + + try { + // 3. Send a request + Message message = client.messages().create(MessageCreateParams.builder() + .model("anthropic.claude-haiku-4-5") + .maxTokens(256) + .addUserMessage("What is 2 + 2? Answer in one word.") + .build()); + + // 4. Verify response + assertNotNull(message, "Response should not be null"); + assertFalse(message.content().isEmpty(), "Response should have content"); + + for (ContentBlock block : message.content()) { + block.text().ifPresent(textBlock -> { + System.out.println("Anthropic response: " + textBlock.text()); + assertFalse(textBlock.text().isBlank(), "Response text should not be blank"); + }); + } + } finally { + client.close(); + } + } + + /** + * Minimal adapter: bridges Anthropic SDK's HttpClient to OCI-signed OkHttpClient. + * This is the glue code consumers write (or copy from examples/). + */ + private static class AnthropicOkHttpAdapter implements HttpClient { + + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); + private final OkHttpClient okHttpClient; + + AnthropicOkHttpAdapter(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + try { + Response okResponse = okHttpClient.newCall(okRequest).execute(); + return new OkHttpResponseAdapter(okResponse); + } catch (IOException e) { + throw new RuntimeException("OCI request failed: " + request.url(), e); + } + } + + @Override + public CompletableFuture executeAsync( + HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + CompletableFuture future = new CompletableFuture<>(); + okHttpClient.newCall(okRequest).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) { + future.complete(new OkHttpResponseAdapter(response)); + } + }); + return future; + } + + @Override + public void close() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + + private Request toOkHttpRequest(HttpRequest request) { + HttpUrl parsedUrl = HttpUrl.parse(request.url()); + if (parsedUrl == null) { + throw new IllegalArgumentException("Invalid URL: " + request.url()); + } + + HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); + var queryParams = request.queryParams(); + for (String key : queryParams.keys()) { + for (String value : queryParams.values(key)) { + urlBuilder.addQueryParameter(key, value); + } + } + + okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); + var headers = request.headers(); + for (String name : headers.names()) { + if ("x-api-key".equalsIgnoreCase(name) || "authorization".equalsIgnoreCase(name)) { + continue; + } + for (String value : headers.values(name)) { + headersBuilder.add(name, value); + } + } + + RequestBody body = null; + HttpRequestBody requestBody = request.body(); + if (requestBody != null) { + body = new RequestBody() { + @Override + public MediaType contentType() { + String ct = requestBody.contentType(); + return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return requestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (OutputStream os = sink.outputStream()) { + requestBody.writeTo(os); + } + } + }; + } + + return new Request.Builder() + .url(urlBuilder.build()) + .headers(headersBuilder.build()) + .method(request.method().name(), body) + .build(); + } + + private static class OkHttpResponseAdapter implements HttpResponse { + private final Response response; + private final Headers headers; + + OkHttpResponseAdapter(Response response) { + this.response = response; + Headers.Builder builder = Headers.builder(); + for (String name : response.headers().names()) { + for (String value : response.headers(name)) { + builder.put(name, value); + } + } + this.headers = builder.build(); + } + + @Override + public int statusCode() { return response.code(); } + + @Override + public Headers headers() { return headers; } + + @Override + public InputStream body() { + ResponseBody b = response.body(); + return b != null ? b.byteStream() : InputStream.nullInputStream(); + } + + @Override + public void close() { response.close(); } + } + } +} diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java new file mode 100644 index 0000000..a77e3e8 --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test: OCI auth library + direct HTTP for Gemini via OCI GenAI. + * + *

    Demonstrates that the OCI-signed OkHttpClient works with raw HTTP calls + * (no vendor SDK needed) — useful when a vendor SDK doesn't support transport injection. + * + *

    To run: + *

    + * oci session authenticate
    + * mvn -pl oci-genai-auth-java-core test -Dtest=GeminiIntegrationTest
    + * 
    + */ +class GeminiIntegrationTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + private static final String BASE_URL = + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat"; + + private static final MediaType JSON = MediaType.parse("application/json"); + + @Test + @Disabled("Requires live OCI session + Gemini endpoint availability") + void gemini_via_direct_http() throws IOException { + // 1. Build OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Build request JSON manually + String requestJson = """ + { + "model": "google.gemini-2.0-flash-001", + "messages": [ + { + "role": "user", + "content": "What is 2 + 2? Answer in one word." + } + ], + "max_tokens": 256 + } + """; + + // 3. Send request + Request request = new Request.Builder() + .url(BASE_URL) + .post(RequestBody.create(requestJson, JSON)) + .build(); + + try (Response response = ociHttpClient.newCall(request).execute()) { + System.out.println("Gemini status: " + response.code()); + String body = response.body() != null ? response.body().string() : ""; + System.out.println("Gemini response: " + body); + + // Accept 200 (success) or 4xx (model not available on PPE) — + // the key validation is that we don't get 401 (auth works) + assertNotEquals(401, response.code(), "Should not get 401 — auth signing should work"); + assertNotEquals(403, response.code(), "Should not get 403 — auth signing should work"); + } + } +} diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java new file mode 100644 index 0000000..92b6ceb --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.genai.auth; + +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; +import com.openai.core.RequestOptions; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; +import com.openai.core.http.Headers; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import okhttp3.*; +import okio.BufferedSink; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test: OCI auth library + OpenAI Java SDK against PPE endpoint. + * + *

    To run: + *

    + * oci session authenticate
    + * mvn -pl oci-genai-auth-java-core test -Dtest=OpenAIIntegrationTest
    + * 
    + */ +class OpenAIIntegrationTest { + + private static final String COMPARTMENT_ID = + "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + + private static final String BASE_URL = + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1"; + + @Test + @Disabled("Requires live OCI session — run: oci session authenticate") + void openai_via_oci_auth_library() { + // 1. Build OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Wrap in OpenAI HttpClient adapter and build client + HttpClient signingHttpClient = new OpenAIOkHttpAdapter(ociHttpClient, BASE_URL); + + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(BASE_URL) + .apiKey("OCI_AUTH") + .build(); + + OpenAIClient client = new OpenAIClientImpl(clientOptions); + + try { + // 3. Send a request + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model("xai.grok-3") + .addUserMessage("What is 2 + 2? Answer in one word.") + .build()); + + // 4. Verify response + assertNotNull(completion, "Response should not be null"); + assertFalse(completion.choices().isEmpty(), "Should have choices"); + + completion.choices().forEach(choice -> + choice.message().content().ifPresent(content -> { + System.out.println("OpenAI response: " + content); + assertFalse(content.isBlank(), "Response content should not be blank"); + })); + } finally { + client.close(); + } + } + + /** + * Minimal adapter: bridges OpenAI SDK's HttpClient to OCI-signed OkHttpClient. + * The OpenAI SDK uses pathSegments instead of full URLs, so we need baseUrl. + */ + private static class OpenAIOkHttpAdapter implements HttpClient { + + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); + private final OkHttpClient okHttpClient; + private final HttpUrl baseUrl; + + OpenAIOkHttpAdapter(OkHttpClient okHttpClient, String baseUrl) { + this.okHttpClient = okHttpClient; + this.baseUrl = HttpUrl.parse(baseUrl); + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + try { + Response okResponse = okHttpClient.newCall(okRequest).execute(); + return new OkHttpResponseAdapter(okResponse); + } catch (IOException e) { + throw new RuntimeException("OCI request failed: " + request.url(), e); + } + } + + @Override + public CompletableFuture executeAsync( + HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + CompletableFuture future = new CompletableFuture<>(); + okHttpClient.newCall(okRequest).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) { + future.complete(new OkHttpResponseAdapter(response)); + } + }); + return future; + } + + @Override + public void close() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + + private Request toOkHttpRequest(HttpRequest request) { + HttpUrl.Builder urlBuilder; + String url = request.url(); + if (url != null && !url.isBlank()) { + HttpUrl parsedUrl = HttpUrl.parse(url); + if (parsedUrl == null) throw new IllegalArgumentException("Invalid URL: " + url); + urlBuilder = parsedUrl.newBuilder(); + } else { + urlBuilder = baseUrl.newBuilder(); + List pathSegments = request.pathSegments(); + if (pathSegments != null) { + for (String segment : pathSegments) { + urlBuilder.addPathSegment(segment); + } + } + } + + var queryParams = request.queryParams(); + for (String key : queryParams.keys()) { + for (String value : queryParams.values(key)) { + urlBuilder.addQueryParameter(key, value); + } + } + + okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); + var headers = request.headers(); + for (String name : headers.names()) { + for (String value : headers.values(name)) { + headersBuilder.add(name, value); + } + } + + RequestBody body = null; + HttpRequestBody requestBody = request.body(); + if (requestBody != null) { + body = new RequestBody() { + @Override + public MediaType contentType() { + String ct = requestBody.contentType(); + return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return requestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (OutputStream os = sink.outputStream()) { + requestBody.writeTo(os); + } + } + }; + } + + return new Request.Builder() + .url(urlBuilder.build()) + .headers(headersBuilder.build()) + .method(request.method().name(), body) + .build(); + } + + private static class OkHttpResponseAdapter implements HttpResponse { + private final Response response; + private final Headers headers; + + OkHttpResponseAdapter(Response response) { + this.response = response; + Headers.Builder builder = Headers.builder(); + for (String name : response.headers().names()) { + for (String value : response.headers(name)) { + builder.put(name, value); + } + } + this.headers = builder.build(); + } + + @Override + public int statusCode() { return response.code(); } + + @Override + public Headers headers() { return headers; } + + @Override + public InputStream body() { + ResponseBody b = response.body(); + return b != null ? b.byteStream() : InputStream.nullInputStream(); + } + + @Override + public void close() { response.close(); } + } + } +} From 7a530744026a461d923c61f9964f025b89ea66a3 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 26 Feb 2026 21:58:30 -0800 Subject: [PATCH 07/16] Fix Gemini endpoint path (/google) and model (gemini-2.5-flash) Update Gemini integration test and example to use correct OCI GenAI Google endpoint (/google/v1beta/models/...:generateContent) and native Gemini request format instead of OpenAI-compatible format. Co-Authored-By: Claude Opus 4.6 --- .../OciGeminiDirectExample.java | 23 ++++++++++-------- .../genai/auth/GeminiIntegrationTest.java | 24 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/examples/gemini-direct-http/OciGeminiDirectExample.java b/examples/gemini-direct-http/OciGeminiDirectExample.java index 6f46914..e67dde7 100644 --- a/examples/gemini-direct-http/OciGeminiDirectExample.java +++ b/examples/gemini-direct-http/OciGeminiDirectExample.java @@ -49,7 +49,8 @@ public class OciGeminiDirectExample { private static final String PROFILE = "DEFAULT"; private static final String REGION = "us-chicago-1"; private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; - private static final String API_PATH = "/20231130/actions/chat"; + private static final String API_PATH = "/google"; + private static final String MODEL = "google.gemini-2.5-flash"; // ──────────────────────────────────────────────────────────────────── private static final MediaType JSON = MediaType.parse("application/json"); @@ -64,27 +65,29 @@ public static void main(String[] args) throws IOException { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - // 2. Resolve the OCI GenAI endpoint + // 2. Resolve the OCI GenAI endpoint for Google Gemini String baseUrl = OciEndpointResolver.resolveBaseUrl( REGION, null, null, API_PATH); + String url = baseUrl + "/v1beta/models/" + MODEL + ":generateContent"; - // 3. Build the request JSON manually (Gemini/OpenAI-compatible format) + // 3. Build the request JSON (Google Gemini generateContent format) String requestJson = """ { - "model": "google.gemini-2.0-flash-001", - "messages": [ + "contents": [ { - "role": "user", - "content": "What is the capital of France? Answer in one sentence." + "parts": [ + { + "text": "What is the capital of France? Answer in one sentence." + } + ] } - ], - "max_tokens": 256 + ] } """; // 4. Send the request using the OCI-signed OkHttpClient Request request = new Request.Builder() - .url(baseUrl) + .url(url) .post(RequestBody.create(requestJson, JSON)) .build(); diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java index a77e3e8..9cd5561 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java @@ -35,12 +35,12 @@ class GeminiIntegrationTest { "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat"; + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/google"; private static final MediaType JSON = MediaType.parse("application/json"); @Test - @Disabled("Requires live OCI session + Gemini endpoint availability") + @Disabled("Requires live OCI session + Gemini endpoint availability on PPE") void gemini_via_direct_http() throws IOException { // 1. Build OCI-signed OkHttpClient OciAuthConfig config = OciAuthConfig.builder() @@ -51,23 +51,27 @@ void gemini_via_direct_http() throws IOException { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - // 2. Build request JSON manually + // 2. Build request JSON (Google Gemini generateContent format) + String model = "google.gemini-2.5-flash"; + String url = BASE_URL + "/v1beta/models/" + model + ":generateContent"; + String requestJson = """ { - "model": "google.gemini-2.0-flash-001", - "messages": [ + "contents": [ { - "role": "user", - "content": "What is 2 + 2? Answer in one word." + "parts": [ + { + "text": "What is 2 + 2? Answer in one word." + } + ] } - ], - "max_tokens": 256 + ] } """; // 3. Send request Request request = new Request.Builder() - .url(BASE_URL) + .url(url) .post(RequestBody.create(requestJson, JSON)) .build(); From 08283cbbfa82e3baed03053bd9f46a4b828debe8 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Mon, 2 Mar 2026 15:04:45 -0800 Subject: [PATCH 08/16] Fix Content-Type charset signing bug and verify Gemini PPE endpoint OkHttp appends "; charset=utf-8" to Content-Type for string bodies. Some OCI endpoints strip the charset before signature verification, causing SIGNATURE_NOT_VALID errors. The fix normalizes Content-Type in OciSigningInterceptor before signing and on the re-attached body. All three providers now verified working on PPE: - Anthropic (claude-haiku-4-5): 200 - OpenAI (xai.grok-3): 200 - Gemini (google.gemini-2.5-flash): 200 Co-Authored-By: Claude Opus 4.6 --- .../OciGeminiDirectExample.java | 1 + .../genai/auth/OciSigningInterceptor.java | 20 ++++++++++++++++--- .../genai/auth/AnthropicIntegrationTest.java | 11 +++------- .../genai/auth/GeminiIntegrationTest.java | 15 +++++--------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/examples/gemini-direct-http/OciGeminiDirectExample.java b/examples/gemini-direct-http/OciGeminiDirectExample.java index e67dde7..726cd32 100644 --- a/examples/gemini-direct-http/OciGeminiDirectExample.java +++ b/examples/gemini-direct-http/OciGeminiDirectExample.java @@ -75,6 +75,7 @@ public static void main(String[] args) throws IOException { { "contents": [ { + "role": "user", "parts": [ { "text": "What is the capital of France? Answer in one sentence." diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java index 6bccaa1..c676597 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java @@ -64,10 +64,19 @@ public Response intercept(Chain chain) throws IOException { URI uri = originalRequest.url().uri(); String method = originalRequest.method(); - // Build the headers map that OCI signing expects + // Build the headers map that OCI signing expects. + // Normalize content-type to strip "; charset=utf-8" that OkHttp appends + // to string bodies — some OCI endpoints strip it before signature verification, + // causing SIGNATURE_NOT_VALID errors. Map> existingHeaders = new HashMap<>(); for (String name : originalRequest.headers().names()) { - existingHeaders.put(name, originalRequest.headers(name)); + List values = originalRequest.headers(name); + if ("content-type".equalsIgnoreCase(name)) { + values = values.stream() + .map(v -> v.replaceAll(";\\s*charset=utf-8", "").trim()) + .toList(); + } + existingHeaders.put(name, values); } // Read the request body for signing (OCI signs the body digest) @@ -95,11 +104,16 @@ public Response intercept(Chain chain) throws IOException { signedRequestBuilder.header(entry.getKey(), entry.getValue()); } - // Re-attach the body (it was consumed during signing) + // Re-attach the body (it was consumed during signing). + // Use byte[] to prevent OkHttp from re-appending "; charset=utf-8". if (bodyBytes != null) { MediaType contentType = originalRequest.body() != null ? originalRequest.body().contentType() : MediaType.parse("application/json"); + // Strip charset to keep content-type consistent with what was signed + if (contentType != null && contentType.charset() != null) { + contentType = MediaType.parse(contentType.type() + "/" + contentType.subtype()); + } signedRequestBuilder.method(method, RequestBody.create(bodyBytes, contentType)); } diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java index e9fab32..a7fb0f4 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java @@ -33,11 +33,11 @@ * Integration test: OCI auth library + Anthropic Java SDK against PPE endpoint. * *

    Validates that oci-genai-auth-java-core produces a correctly signed - * OkHttpClient that works with the Anthropic SDK out of the box. + * OkHttpClient that works with the Anthropic SDK. * *

    To run: *

    - * oci session authenticate          # refresh token
    + * oci session authenticate
      * mvn -pl oci-genai-auth-java-core test -Dtest=AnthropicIntegrationTest
      * 
    */ @@ -52,7 +52,6 @@ class AnthropicIntegrationTest { @Test @Disabled("Requires live OCI session — run: oci session authenticate") void anthropic_via_oci_auth_library() { - // 1. Build OCI-signed OkHttpClient using the auth library OciAuthConfig config = OciAuthConfig.builder() .authType("security_token") .profile("DEFAULT") @@ -61,7 +60,6 @@ void anthropic_via_oci_auth_library() { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - // 2. Wrap in Anthropic HttpClient adapter and build client HttpClient signingHttpClient = new AnthropicOkHttpAdapter(ociHttpClient); ClientOptions clientOptions = ClientOptions.builder() @@ -73,14 +71,12 @@ void anthropic_via_oci_auth_library() { AnthropicClient client = new AnthropicClientImpl(clientOptions); try { - // 3. Send a request Message message = client.messages().create(MessageCreateParams.builder() .model("anthropic.claude-haiku-4-5") .maxTokens(256) .addUserMessage("What is 2 + 2? Answer in one word.") .build()); - // 4. Verify response assertNotNull(message, "Response should not be null"); assertFalse(message.content().isEmpty(), "Response should have content"); @@ -96,8 +92,7 @@ void anthropic_via_oci_auth_library() { } /** - * Minimal adapter: bridges Anthropic SDK's HttpClient to OCI-signed OkHttpClient. - * This is the glue code consumers write (or copy from examples/). + * Adapter: bridges Anthropic SDK's HttpClient to OCI-signed OkHttpClient. */ private static class AnthropicOkHttpAdapter implements HttpClient { diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java index 9cd5561..799a1d9 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java @@ -40,9 +40,8 @@ class GeminiIntegrationTest { private static final MediaType JSON = MediaType.parse("application/json"); @Test - @Disabled("Requires live OCI session + Gemini endpoint availability on PPE") + @Disabled("Requires live OCI session — run: oci session authenticate") void gemini_via_direct_http() throws IOException { - // 1. Build OCI-signed OkHttpClient OciAuthConfig config = OciAuthConfig.builder() .authType("security_token") .profile("DEFAULT") @@ -51,7 +50,6 @@ void gemini_via_direct_http() throws IOException { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - // 2. Build request JSON (Google Gemini generateContent format) String model = "google.gemini-2.5-flash"; String url = BASE_URL + "/v1beta/models/" + model + ":generateContent"; @@ -59,6 +57,7 @@ void gemini_via_direct_http() throws IOException { { "contents": [ { + "role": "user", "parts": [ { "text": "What is 2 + 2? Answer in one word." @@ -69,21 +68,17 @@ void gemini_via_direct_http() throws IOException { } """; - // 3. Send request Request request = new Request.Builder() .url(url) .post(RequestBody.create(requestJson, JSON)) .build(); try (Response response = ociHttpClient.newCall(request).execute()) { - System.out.println("Gemini status: " + response.code()); String body = response.body() != null ? response.body().string() : ""; - System.out.println("Gemini response: " + body); + System.out.println("Gemini response (" + response.code() + "): " + body); - // Accept 200 (success) or 4xx (model not available on PPE) — - // the key validation is that we don't get 401 (auth works) - assertNotEquals(401, response.code(), "Should not get 401 — auth signing should work"); - assertNotEquals(403, response.code(), "Should not get 403 — auth signing should work"); + assertNotEquals(401, response.code(), "Auth signing should work"); + assertNotEquals(403, response.code(), "Auth signing should work"); } } } From 2ae12064c4e9d1b23552f50f37c12eb408b6d5b9 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 5 Mar 2026 14:58:45 -0800 Subject: [PATCH 09/16] Upgrade dependencies and add LICENSE.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OCI SDK: 3.57.2 → 3.72.1 - OkHttp: 4.12.0 → 5.3.2 - SLF4J: 2.0.16 → 2.0.17 - Add LICENSE.txt (UPL v1.0) All unit and integration tests verified passing. Co-Authored-By: Claude Opus 4.6 --- LICENSE.txt | 2 +- oci-genai-auth-java-bom/pom.xml | 6 +++--- pom.xml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index bb91ea7..9e2107f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2026 Oracle and/or its affiliates. +Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. The Universal Permissive License (UPL), Version 1.0 diff --git a/oci-genai-auth-java-bom/pom.xml b/oci-genai-auth-java-bom/pom.xml index 208dd45..d05e03c 100644 --- a/oci-genai-auth-java-bom/pom.xml +++ b/oci-genai-auth-java-bom/pom.xml @@ -26,11 +26,11 @@ 0.1.0-SNAPSHOT - 3.57.2 + 3.72.1 - 4.12.0 - 2.0.16 + 5.3.2 + 2.0.17
    diff --git a/pom.xml b/pom.xml index 8e25652..3c1063b 100644 --- a/pom.xml +++ b/pom.xml @@ -42,11 +42,11 @@ 0.1.0-SNAPSHOT - 3.57.2 + 3.72.1 - 4.12.0 - 2.0.16 + 5.3.2 + 2.0.17 5.11.4 From e31c1ad8793fbbad4ea35f20540f1df580b65d05 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 5 Mar 2026 15:20:41 -0800 Subject: [PATCH 10/16] Add THIRD_PARTY_LICENSES.txt for direct runtime dependencies Co-Authored-By: Claude Opus 4.6 --- THIRD_PARTY_LICENSES.txt | 241 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 THIRD_PARTY_LICENSES.txt diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt new file mode 100644 index 0000000..195c1ee --- /dev/null +++ b/THIRD_PARTY_LICENSES.txt @@ -0,0 +1,241 @@ +THIRD-PARTY SOFTWARE LICENSES + +The following third-party software is used as direct dependencies by this +project. Only compile-scope (runtime) dependencies are listed below. Test-scope +dependencies (JUnit, Mockito, MockWebServer, etc.) are not redistributed. + +================================================================================ + +1. OCI Java SDK Common + Group: com.oracle.oci.sdk + Artifact: oci-java-sdk-common + Version: 3.72.1 + License: Universal Permissive License v 1.0 (UPL-1.0) OR Apache License 2.0 + URL: https://github.com/oracle/oci-java-sdk + +2. OkHttp + Group: com.squareup.okhttp3 + Artifact: okhttp + Version: 5.3.2 + License: Apache License 2.0 + URL: https://github.com/square/okhttp + +3. OkHttp Logging Interceptor + Group: com.squareup.okhttp3 + Artifact: logging-interceptor + Version: 5.3.2 + License: Apache License 2.0 + URL: https://github.com/square/okhttp + +4. SLF4J API + Group: org.slf4j + Artifact: slf4j-api + Version: 2.0.17 + License: MIT License + URL: https://www.slf4j.org + +================================================================================ + +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 the 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 the 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 any 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 + +================================================================================ + +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. + +================================================================================ From 17b8c168ec4b25bcb00303f6117719bd1db60440 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Thu, 5 Mar 2026 15:39:01 -0800 Subject: [PATCH 11/16] Update copyright year from 2025 to 2026 across all files Co-Authored-By: Claude Opus 4.6 --- LICENSE.txt | 2 +- README.md | 2 +- examples/anthropic/OciAnthropicExample.java | 2 +- examples/gemini-direct-http/OciGeminiDirectExample.java | 2 +- examples/openai/OciOpenAIExample.java | 2 +- oci-genai-auth-java-bom/pom.xml | 2 +- oci-genai-auth-java-core/pom.xml | 2 +- .../src/main/java/com/oracle/genai/auth/OciAuthConfig.java | 2 +- .../src/main/java/com/oracle/genai/auth/OciAuthException.java | 2 +- .../main/java/com/oracle/genai/auth/OciAuthProviderFactory.java | 2 +- .../main/java/com/oracle/genai/auth/OciEndpointResolver.java | 2 +- .../main/java/com/oracle/genai/auth/OciHeaderInterceptor.java | 2 +- .../main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java | 2 +- .../main/java/com/oracle/genai/auth/OciSigningInterceptor.java | 2 +- .../java/com/oracle/genai/auth/AnthropicIntegrationTest.java | 2 +- .../test/java/com/oracle/genai/auth/GeminiIntegrationTest.java | 2 +- .../java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java | 2 +- .../java/com/oracle/genai/auth/OciEndpointResolverTest.java | 2 +- .../java/com/oracle/genai/auth/OciHeaderInterceptorTest.java | 2 +- .../java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java | 2 +- .../java/com/oracle/genai/auth/OciSigningInterceptorTest.java | 2 +- .../test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java | 2 +- pom.xml | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 9e2107f..92e1920 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. The Universal Permissive License (UPL), Version 1.0 diff --git a/README.md b/README.md index 9a76a59..0f53fd5 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,6 @@ mvn dependency:tree -pl oci-genai-auth-java-core ## License -Copyright (c) 2025 Oracle and/or its affiliates. +Copyright (c) 2026 Oracle and/or its affiliates. Released under the [Universal Permissive License v1.0](https://oss.oracle.com/licenses/upl/). diff --git a/examples/anthropic/OciAnthropicExample.java b/examples/anthropic/OciAnthropicExample.java index 73bfca3..8f8b441 100644 --- a/examples/anthropic/OciAnthropicExample.java +++ b/examples/anthropic/OciAnthropicExample.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/gemini-direct-http/OciGeminiDirectExample.java b/examples/gemini-direct-http/OciGeminiDirectExample.java index 726cd32..48ed80e 100644 --- a/examples/gemini-direct-http/OciGeminiDirectExample.java +++ b/examples/gemini-direct-http/OciGeminiDirectExample.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/OciOpenAIExample.java b/examples/openai/OciOpenAIExample.java index 8fc09ff..7ab7bdf 100644 --- a/examples/openai/OciOpenAIExample.java +++ b/examples/openai/OciOpenAIExample.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-bom/pom.xml b/oci-genai-auth-java-bom/pom.xml index d05e03c..c751d98 100644 --- a/oci-genai-auth-java-bom/pom.xml +++ b/oci-genai-auth-java-bom/pom.xml @@ -1,6 +1,6 @@ diff --git a/oci-genai-auth-java-core/pom.xml b/oci-genai-auth-java-core/pom.xml index c87d708..0d01fce 100644 --- a/oci-genai-auth-java-core/pom.xml +++ b/oci-genai-auth-java-core/pom.xml @@ -1,6 +1,6 @@ diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java index 955b292..4e8609a 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java index c34ca71..345fdbc 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java index 76b7cfb..81b5c15 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthProviderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java index 9028ee9..4b10065 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java index 844e24c..1d61ea6 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciHeaderInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java index b43619b..a70bd6a 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java index c676597..78328b6 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciSigningInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java index a7fb0f4..a08ffa9 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java index 799a1d9..5249152 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java index 6fbd39a..90f51f2 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciAuthProviderFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java index 8c0fc6a..b9e3bd8 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java index 27ed1ab..42e740f 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciHeaderInterceptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java index 46931d6..4be4f9f 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciOkHttpClientFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java index 8bbbe2f..9cbee50 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciSigningInterceptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java index 92b6ceb..858d78e 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Oracle and/or its affiliates. + * 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/pom.xml b/pom.xml index 3c1063b..4a4a50d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ From 3bbc765e873cb69cdb8cf88f482bf7094b5e9555 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Wed, 11 Mar 2026 10:40:10 -0700 Subject: [PATCH 12/16] Harden endpoint validation, redact logging, and add Responses API test - OciEndpointResolver: enforce HTTPS + OCI domain (*.oraclecloud.com) on both baseUrl and serviceEndpoint using java.net.URI parsing; reject user-info and missing hosts - OciOkHttpClientFactory: redact Authorization, x-content-sha256, and opc-request-id headers in debug logging; use HEADERS level instead of BODY - Replace hardcoded OCIDs with env vars in all integration tests - Add OpenAIResponsesIntegrationTest for Responses API via OCI auth - Upgrade openai-java test dependency from 0.40.0 to 4.26.0 - Update .gitignore for CLAUDE.md, .claude/, and scratch/ Co-Authored-By: Claude Opus 4.6 --- .gitignore | 7 + README.md | 6 +- oci-genai-auth-java-core/pom.xml | 2 +- .../com/oracle/genai/auth/OciAuthConfig.java | 2 +- .../genai/auth/OciEndpointResolver.java | 40 ++- .../genai/auth/OciOkHttpClientFactory.java | 20 +- .../genai/auth/AnthropicIntegrationTest.java | 6 +- .../genai/auth/GeminiIntegrationTest.java | 6 +- .../genai/auth/OciEndpointResolverTest.java | 65 ++++- .../genai/auth/OpenAIIntegrationTest.java | 6 +- .../auth/OpenAIResponsesIntegrationTest.java | 253 ++++++++++++++++++ 11 files changed, 387 insertions(+), 26 deletions(-) create mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIResponsesIntegrationTest.java diff --git a/.gitignore b/.gitignore index 8f74b63..04e3d24 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,10 @@ dependency-reduced-pom.xml # Logs *.log + +# Claude Code +CLAUDE.md +.claude/ + +# Scratch files +scratch/ diff --git a/README.md b/README.md index 0f53fd5..0766c50 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,9 @@ String url = OciEndpointResolver.resolveBaseUrl(null, "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", null, "/20231130/actions/chat"); -// From explicit base URL (used as-is) +// From explicit base URL (used as-is; must be an OCI HTTPS URL) String url = OciEndpointResolver.resolveBaseUrl(null, null, - "https://custom-endpoint.example.com/v1", null); + "https://custom-endpoint.oci.oraclecloud.com/v1", null); ``` Resolution priority: `baseUrl` > `serviceEndpoint` > `region`. @@ -131,7 +131,7 @@ Resolution priority: `baseUrl` > `serviceEndpoint` > `region`. | `profile` | OCI config profile name (default: `DEFAULT`) | No | | `compartmentId` | OCI compartment OCID | Yes (for GenAI endpoints) | | `region` | OCI region code (e.g., `us-chicago-1`) | No (for endpoint resolution) | -| `baseUrl` | Fully qualified endpoint override | No | +| `baseUrl` | Fully qualified OCI endpoint override (`*.oraclecloud.com`) | No | | `timeout` | Request timeout (default: 2 minutes) | No | ## Examples diff --git a/oci-genai-auth-java-core/pom.xml b/oci-genai-auth-java-core/pom.xml index 0d01fce..9626a8d 100644 --- a/oci-genai-auth-java-core/pom.xml +++ b/oci-genai-auth-java-core/pom.xml @@ -63,7 +63,7 @@ com.openai openai-java - 0.40.0 + 4.26.0 test diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java index 4e8609a..4c42b51 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciAuthConfig.java @@ -83,7 +83,7 @@ private Builder() {} /** Sets the OCI region code (e.g., {@code "us-chicago-1"}). */ public Builder region(String region) { this.region = region; return this; } - /** Sets the fully qualified base URL (overrides region-based resolution). */ + /** Sets the fully qualified OCI base URL (overrides region-based resolution). */ public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } /** Sets the OCI compartment OCID. */ diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java index 4b10065..6891698 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciEndpointResolver.java @@ -5,14 +5,17 @@ */ package com.oracle.genai.auth; +import java.net.URI; +import java.util.Locale; + /** * Resolves the OCI Generative AI service base URL from region, service endpoint, * or an explicit base URL override. * *

    Resolution priority (highest to lowest): *

      - *
    1. {@code baseUrl} — fully qualified URL, used as-is
    2. - *
    3. {@code serviceEndpoint} — service root; the caller-supplied {@code apiPath} is appended
    4. + *
    5. {@code baseUrl} — fully qualified OCI URL, used as-is (HTTPS + OCI domain required)
    6. + *
    7. {@code serviceEndpoint} — service root; must be an OCI domain ({@code *.oraclecloud.com})
    8. *
    9. {@code region} — auto-derives the service endpoint from the OCI region code
    10. *
    */ @@ -21,6 +24,8 @@ public final class OciEndpointResolver { private static final String SERVICE_ENDPOINT_TEMPLATE = "https://inference.generativeai.%s.oci.oraclecloud.com"; + private static final String OCI_DOMAIN_SUFFIX = ".oraclecloud.com"; + private OciEndpointResolver() { // utility class } @@ -29,18 +34,20 @@ private OciEndpointResolver() { * Resolves a base URL with a caller-supplied API path suffix. * * @param region OCI region code (e.g., "us-chicago-1") - * @param serviceEndpoint service root URL (without API path) - * @param baseUrl fully qualified URL override (used as-is when provided) + * @param serviceEndpoint service root URL (without API path; must be an OCI domain) + * @param baseUrl fully qualified OCI URL override (used as-is when provided) * @param apiPath the API path to append to the service endpoint * @return the resolved base URL * @throws IllegalArgumentException if none of region, serviceEndpoint, or baseUrl is provided */ public static String resolveBaseUrl(String region, String serviceEndpoint, String baseUrl, String apiPath) { if (baseUrl != null && !baseUrl.isBlank()) { + validateUrl(baseUrl, "baseUrl"); return stripTrailingSlash(baseUrl); } if (serviceEndpoint != null && !serviceEndpoint.isBlank()) { + validateUrl(serviceEndpoint, "serviceEndpoint"); return stripTrailingSlash(serviceEndpoint) + apiPath; } @@ -62,6 +69,31 @@ public static String buildServiceEndpoint(String region) { return String.format(SERVICE_ENDPOINT_TEMPLATE, region); } + private static void validateUrl(String url, String paramName) { + URI uri; + try { + uri = URI.create(url); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(paramName + " must be a valid absolute HTTPS URL: " + url, e); + } + + if (uri.getScheme() == null || !"https".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException(paramName + " must use HTTPS: " + url); + } + + String host = uri.getHost(); + if (host == null || host.isBlank()) { + throw new IllegalArgumentException(paramName + " must include a valid host: " + url); + } + if (uri.getRawUserInfo() != null) { + throw new IllegalArgumentException(paramName + " must not include user-info: " + url); + } + if (!host.toLowerCase(Locale.ROOT).endsWith(OCI_DOMAIN_SUFFIX)) { + throw new IllegalArgumentException( + paramName + " must be an OCI domain (*" + OCI_DOMAIN_SUFFIX + "): " + url); + } + } + private static String stripTrailingSlash(String url) { return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; } diff --git a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java index a70bd6a..d5d8b03 100644 --- a/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java +++ b/oci-genai-auth-java-core/src/main/java/com/oracle/genai/auth/OciOkHttpClientFactory.java @@ -14,6 +14,7 @@ import java.time.Duration; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * Factory for creating OkHttp clients pre-configured with OCI signing and header interceptors. @@ -37,6 +38,10 @@ public final class OciOkHttpClientFactory { private static final Logger LOG = LoggerFactory.getLogger(OciOkHttpClientFactory.class); private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(2); + /** Headers that are redacted from debug logging to prevent credential leakage. */ + private static final Set REDACTED_HEADERS = Set.of( + "authorization", "x-content-sha256", "opc-request-id"); + private OciOkHttpClientFactory() { // utility class } @@ -86,8 +91,19 @@ public static OkHttpClient create( if (logRequests) { HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor( - message -> LOG.debug("{}", message)); - loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + message -> { + // Redact sensitive headers (Authorization, x-content-sha256) + // to prevent OCI signing credentials from appearing in logs. + String redacted = message; + for (String header : REDACTED_HEADERS) { + if (redacted.toLowerCase().startsWith(header + ":")) { + redacted = header + ": [REDACTED]"; + break; + } + } + LOG.debug("{}", redacted); + }); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); builder.addInterceptor(loggingInterceptor); } diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java index a08ffa9..3e51ee0 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java @@ -44,10 +44,12 @@ class AnthropicIntegrationTest { private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/anthropic"; + System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") + + "/anthropic"; @Test @Disabled("Requires live OCI session — run: oci session authenticate") diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java index 5249152..77151ea 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java @@ -32,10 +32,12 @@ class GeminiIntegrationTest { private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/google"; + System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") + + "/google"; private static final MediaType JSON = MediaType.parse("application/json"); diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java index b9e3bd8..8331dd4 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OciEndpointResolverTest.java @@ -31,35 +31,82 @@ void resolveBaseUrl_fromServiceEndpoint() { @Test void resolveBaseUrl_fromBaseUrl() { String url = OciEndpointResolver.resolveBaseUrl( - null, null, "https://custom-endpoint.example.com/v1", "/ignored"); - assertEquals("https://custom-endpoint.example.com/v1", url); + null, null, "https://custom-endpoint.oci.oraclecloud.com/v1", "/ignored"); + assertEquals("https://custom-endpoint.oci.oraclecloud.com/v1", url); } @Test void resolveBaseUrl_baseUrlTakesPrecedence() { String url = OciEndpointResolver.resolveBaseUrl( "us-chicago-1", - "https://service.example.com", - "https://override.example.com/v1", + "https://service.oci.oraclecloud.com", + "https://override.oci.oraclecloud.com/v1", "/v1/test"); - assertEquals("https://override.example.com/v1", url); + assertEquals("https://override.oci.oraclecloud.com/v1", url); } @Test void resolveBaseUrl_serviceEndpointTakesPrecedenceOverRegion() { String url = OciEndpointResolver.resolveBaseUrl( "us-chicago-1", - "https://custom-service.example.com", + "https://custom-service.oci.oraclecloud.com", null, "/v1/test"); - assertEquals("https://custom-service.example.com/v1/test", url); + assertEquals("https://custom-service.oci.oraclecloud.com/v1/test", url); } @Test void resolveBaseUrl_stripsTrailingSlash() { String url = OciEndpointResolver.resolveBaseUrl( - null, "https://service.example.com/", null, "/v1/test"); - assertEquals("https://service.example.com/v1/test", url); + null, "https://service.oraclecloud.com/", null, "/v1/test"); + assertEquals("https://service.oraclecloud.com/v1/test", url); + } + + @Test + void resolveBaseUrl_serviceEndpointRejectsNonOciDomain() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl( + null, "https://evil.example.com", null, "/v1/test")); + } + + @Test + void resolveBaseUrl_serviceEndpointAcceptsPpeDomain() { + String url = OciEndpointResolver.resolveBaseUrl( + null, + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com", + null, + "/v1/test"); + assertEquals( + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1/test", + url); + } + + @Test + void resolveBaseUrl_baseUrlRejectsNonOciDomain() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl( + null, null, "https://custom-proxy.example.com/v1", "/ignored")); + } + + @Test + void resolveBaseUrl_baseUrlRejectsMissingHost() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl( + null, null, "https://", "/ignored")); + } + + @Test + void resolveBaseUrl_baseUrlRejectsUserInfo() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl( + null, null, "https://user:pass@inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1", "/ignored")); + } + + @Test + void resolveBaseUrl_serviceEndpointRejectsMissingHost() { + assertThrows(IllegalArgumentException.class, () -> + OciEndpointResolver.resolveBaseUrl( + null, "https://", null, "/v1/test")); } @Test diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java index 858d78e..5f59e44 100644 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIIntegrationTest.java @@ -41,10 +41,12 @@ class OpenAIIntegrationTest { private static final String COMPARTMENT_ID = - "ocid1.tenancy.oc1..aaaaaaaaumuuscymm6yb3wsbaicfx3mjhesghplvrvamvbypyehh5pgaasna"; + System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); private static final String BASE_URL = - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1"; + System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") + + "/20231130/actions/v1"; @Test @Disabled("Requires live OCI session — run: oci session authenticate") diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIResponsesIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIResponsesIntegrationTest.java new file mode 100644 index 0000000..b79f8b5 --- /dev/null +++ b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/OpenAIResponsesIntegrationTest.java @@ -0,0 +1,253 @@ +/* + * 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/ + */ +package com.oracle.genai.auth; + +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; +import com.openai.core.RequestOptions; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpRequestBody; +import com.openai.core.http.HttpResponse; +import com.openai.core.http.Headers; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseOutputItem; +import okhttp3.*; +import okio.BufferedSink; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test: OCI auth library + OpenAI Responses API against PPE endpoint. + * + *

    Tests the Responses API (newer replacement for Chat Completions) which supports + * compaction for long-running conversations. + * + *

    To run: + *

    + * oci session authenticate
    + * mvn -pl oci-genai-auth-java-core test -Dtest=OpenAIResponsesIntegrationTest
    + * 
    + */ +class OpenAIResponsesIntegrationTest { + + private static final String COMPARTMENT_ID = + System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); + + private static final String BASE_URL = + System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", + "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") + + "/20231130/actions/v1"; + + @Test + @Disabled("Requires live OCI session — run: oci session authenticate") + void responses_api_via_oci_auth_library() { + // 1. Build OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Wrap in OpenAI HttpClient adapter and build client + HttpClient signingHttpClient = new OpenAIOkHttpAdapter(ociHttpClient, BASE_URL); + + ClientOptions clientOptions = ClientOptions.builder() + .httpClient(signingHttpClient) + .baseUrl(BASE_URL) + .apiKey("OCI_AUTH") + .build(); + + OpenAIClient client = new OpenAIClientImpl(clientOptions); + + try { + // 3. Send a Responses API request + ResponseCreateParams params = ResponseCreateParams.builder() + .model("xai.grok-3") + .input("What is 2 + 2? Answer in one word.") + .build(); + + Response response = client.responses().create(params); + + // 4. Verify response + assertNotNull(response, "Response should not be null"); + assertNotNull(response.output(), "Output should not be null"); + assertFalse(response.output().isEmpty(), "Output should not be empty"); + + // 5. Extract text from output + for (ResponseOutputItem item : response.output()) { + item.message().ifPresent(message -> { + for (var content : message.content()) { + content.outputText().ifPresent(text -> { + System.out.println("Responses API output: " + text.text()); + assertFalse(text.text().isBlank(), "Response text should not be blank"); + }); + } + }); + } + } finally { + client.close(); + } + } + + /** + * Minimal adapter: bridges OpenAI SDK's HttpClient to OCI-signed OkHttpClient. + * The OpenAI SDK uses pathSegments instead of full URLs, so we need baseUrl. + */ + private static class OpenAIOkHttpAdapter implements HttpClient { + + private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); + private final OkHttpClient okHttpClient; + private final HttpUrl baseUrl; + + OpenAIOkHttpAdapter(OkHttpClient okHttpClient, String baseUrl) { + this.okHttpClient = okHttpClient; + this.baseUrl = HttpUrl.parse(baseUrl); + } + + @Override + public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + try { + okhttp3.Response okResponse = okHttpClient.newCall(okRequest).execute(); + return new OkHttpResponseAdapter(okResponse); + } catch (IOException e) { + throw new RuntimeException("OCI request failed: " + request.url(), e); + } + } + + @Override + public CompletableFuture executeAsync( + HttpRequest request, RequestOptions requestOptions) { + Request okRequest = toOkHttpRequest(request); + CompletableFuture future = new CompletableFuture<>(); + okHttpClient.newCall(okRequest).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + future.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, okhttp3.Response response) { + future.complete(new OkHttpResponseAdapter(response)); + } + }); + return future; + } + + @Override + public void close() { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + + private Request toOkHttpRequest(HttpRequest request) { + HttpUrl.Builder urlBuilder; + String url = request.url(); + if (url != null && !url.isBlank()) { + HttpUrl parsedUrl = HttpUrl.parse(url); + if (parsedUrl == null) throw new IllegalArgumentException("Invalid URL: " + url); + urlBuilder = parsedUrl.newBuilder(); + } else { + urlBuilder = baseUrl.newBuilder(); + List pathSegments = request.pathSegments(); + if (pathSegments != null) { + for (String segment : pathSegments) { + urlBuilder.addPathSegment(segment); + } + } + } + + var queryParams = request.queryParams(); + for (String key : queryParams.keys()) { + for (String value : queryParams.values(key)) { + urlBuilder.addQueryParameter(key, value); + } + } + + okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); + var headers = request.headers(); + for (String name : headers.names()) { + for (String value : headers.values(name)) { + headersBuilder.add(name, value); + } + } + + RequestBody body = null; + HttpRequestBody requestBody = request.body(); + if (requestBody != null) { + body = new RequestBody() { + @Override + public MediaType contentType() { + String ct = requestBody.contentType(); + return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return requestBody.contentLength(); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (OutputStream os = sink.outputStream()) { + requestBody.writeTo(os); + } + } + }; + } + + return new Request.Builder() + .url(urlBuilder.build()) + .headers(headersBuilder.build()) + .method(request.method().name(), body) + .build(); + } + + private static class OkHttpResponseAdapter implements HttpResponse { + private final okhttp3.Response response; + private final Headers headers; + + OkHttpResponseAdapter(okhttp3.Response response) { + this.response = response; + Headers.Builder builder = Headers.builder(); + for (String name : response.headers().names()) { + for (String value : response.headers(name)) { + builder.put(name, value); + } + } + this.headers = builder.build(); + } + + @Override + public int statusCode() { return response.code(); } + + @Override + public Headers headers() { return headers; } + + @Override + public InputStream body() { + ResponseBody b = response.body(); + return b != null ? b.byteStream() : InputStream.nullInputStream(); + } + + @Override + public void close() { response.close(); } + } + } +} From 7fbcfb25e6c353554975f5194435458f4e4fc97b Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Fri, 20 Mar 2026 10:21:59 -0700 Subject: [PATCH 13/16] Align repo structure with Python counterpart for initial release - Restructure examples into agenthub/ (Responses API) and partner/ (Chat Completions) to match oci-genai-auth-python layout - Remove Anthropic and Gemini examples and integration tests from main branch (preserved in feature/* branches) - Remove Anthropic SDK test dependency - Add repo scaffolding: CONTRIBUTING.md, SECURITY.md, PR template, GitHub Actions CI workflow (Java 17 + 21) - Update README with AgentHub vs Partner documentation pattern, API key auth instructions, and OCI IAM policy examples - Update .gitignore with additional patterns Co-Authored-By: Claude Opus 4.6 --- .github/workflows/run-all-check.yaml | 33 +++ .gitignore | 9 +- CONTRIBUTING.md | 2 - README.md | 189 +++++++------- .../openai/QuickstartResponsesApiKey.java | 46 ++++ .../openai/QuickstartResponsesOciIam.java | 61 +++++ .../openai/responses/CreateResponse.java | 54 ++++ .../openai/responses/StreamingTextDelta.java | 58 +++++ .../openai/tools/FunctionCalling.java | 105 ++++++++ examples/agenthub/openai/tools/WebSearch.java | 57 +++++ examples/anthropic/OciAnthropicExample.java | 93 ------- .../OciGeminiDirectExample.java | 102 -------- examples/openai/OciOpenAIExample.java | 88 ------- .../partner/openai/BasicChatCompletion.java | 65 +++++ .../openai/BasicChatCompletionApiKey.java | 45 ++++ .../openai/StreamingChatCompletion.java | 61 +++++ .../openai/ToolCallChatCompletion.java | 113 +++++++++ oci-genai-auth-java-core/pom.xml | 8 - .../genai/auth/AnthropicIntegrationTest.java | 231 ------------------ .../genai/auth/GeminiIntegrationTest.java | 86 ------- 20 files changed, 807 insertions(+), 699 deletions(-) create mode 100644 .github/workflows/run-all-check.yaml create mode 100644 examples/agenthub/openai/QuickstartResponsesApiKey.java create mode 100644 examples/agenthub/openai/QuickstartResponsesOciIam.java create mode 100644 examples/agenthub/openai/responses/CreateResponse.java create mode 100644 examples/agenthub/openai/responses/StreamingTextDelta.java create mode 100644 examples/agenthub/openai/tools/FunctionCalling.java create mode 100644 examples/agenthub/openai/tools/WebSearch.java delete mode 100644 examples/anthropic/OciAnthropicExample.java delete mode 100644 examples/gemini-direct-http/OciGeminiDirectExample.java delete mode 100644 examples/openai/OciOpenAIExample.java create mode 100644 examples/partner/openai/BasicChatCompletion.java create mode 100644 examples/partner/openai/BasicChatCompletionApiKey.java create mode 100644 examples/partner/openai/StreamingChatCompletion.java create mode 100644 examples/partner/openai/ToolCallChatCompletion.java delete mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java delete mode 100644 oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java diff --git a/.github/workflows/run-all-check.yaml b/.github/workflows/run-all-check.yaml new file mode 100644 index 0000000..c2aa986 --- /dev/null +++ b/.github/workflows/run-all-check.yaml @@ -0,0 +1,33 @@ +# This workflow will install Java dependencies, run tests and lint +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +name: Unit test and build check + +on: + workflow_dispatch: + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java-version: ["17", "21"] + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + - name: Build and test + run: mvn clean verify --batch-mode --no-transfer-progress + - name: Verify no vendor SDK in compile scope + run: | + mvn dependency:tree -pl oci-genai-auth-java-core --batch-mode --no-transfer-progress | \ + grep -v "test" | grep -v "INFO" | grep -v "WARNING" | \ + (! grep -E "anthropic|openai|google-cloud") diff --git a/.gitignore b/.gitignore index 04e3d24..584488e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Mac +.DS_Store + # Build output target/ @@ -10,9 +13,9 @@ target/ .vscode/ *.swp *.swo +*.orig # OS files -.DS_Store Thumbs.db # Maven @@ -21,6 +24,10 @@ dependency-reduced-pom.xml # Logs *.log +# Environments +.env +.envrc + # Claude Code CLAUDE.md .claude/ 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/README.md b/README.md index 0766c50..4a9bf27 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,41 @@ -# OCI GenAI Auth for Java +# oci-genai-auth-java -Vendor-neutral OCI authentication and request signing library for Java. Provides an OCI-signed `OkHttpClient` that you can plug into **any** vendor SDK or use directly with raw HTTP. +The **OCI GenAI Auth** Java 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. -## What This Library Does +## Table of Contents -- **OCI IAM request signing** — RSA-SHA256 signatures on every request, including body digest for POST/PUT -- **Auth provider factory** — supports `oci_config`, `security_token`, `instance_principal`, and `resource_principal` -- **Header injection** — automatically adds `CompartmentId` and custom headers -- **Endpoint resolution** — derives OCI GenAI service URLs from region codes -- **Token refresh** — handled automatically by the underlying OCI Java SDK auth providers +- [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) +- [Building from Source](#building-from-source) -## What This Library Does NOT Do +## Before you start -- Does **not** generate provider request/response models (no OpenAPI/codegen) -- Does **not** wrap or re-export any vendor SDK (OpenAI, Anthropic, Gemini, etc.) -- Does **not** include provider-specific shim classes +**Important!** -This is an **auth utilities library**. Vendor SDK integration is shown in the [examples/](examples/) directory. +Note that this package, as well as API keys 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. + +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-java` SDK like before. Just update the `base_url`, create API keys in your OCI console, ensure 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' } +``` ## Installation @@ -43,123 +62,123 @@ Requires **Java 17+** and **Maven 3.8+**. ``` -## Quick Start +## Using OCI IAM Auth -### Using OciAuthConfig (recommended) +Use OCI IAM auth when you want to sign requests with your OCI profile (session/user/resource/instance principal) instead of API keys. ```java import com.oracle.genai.auth.OciAuthConfig; import com.oracle.genai.auth.OciOkHttpClientFactory; -import okhttp3.OkHttpClient; +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; OciAuthConfig config = OciAuthConfig.builder() .authType("security_token") .profile("DEFAULT") - .compartmentId("ocid1.compartment.oc1..xxx") + .compartmentId("ocid1.compartment.oc1..aaaaaaaaexample") .build(); -OkHttpClient client = OciOkHttpClientFactory.build(config); -// Use this client with any vendor SDK that accepts an OkHttpClient, -// or make direct HTTP calls — every request is signed automatically. -``` +OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); -### Direct factory method +// Plug the OCI-signed OkHttpClient into the OpenAI SDK +OpenAIClient client = new OpenAIClientImpl( + ClientOptions.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1") + .apiKey("not-used") + .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) + .build()); +``` -```java -import com.oracle.genai.auth.OciAuthProviderFactory; -import com.oracle.genai.auth.OciOkHttpClientFactory; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +## Using API Key Auth -BasicAuthenticationDetailsProvider authProvider = - OciAuthProviderFactory.create("security_token", "DEFAULT"); +Use OCI Generative AI API Keys if you want a direct API-key workflow with the OpenAI SDK. -OkHttpClient client = OciOkHttpClientFactory.create(authProvider, "ocid1.compartment.oc1..xxx"); +```java +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; + +OpenAIClient client = new OpenAIClientImpl( + ClientOptions.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") + .apiKey(System.getenv("OPENAI_API_KEY")) + .build()); ``` -## Authentication Types +## Using AgentHub APIs (non-passthrough) -| Auth Type | Use Case | -|-----------|----------| -| `oci_config` | Local development with API key in `~/.oci/config` | -| `security_token` | Local development with OCI CLI session token | -| `instance_principal` | OCI Compute instances with dynamic group policies | -| `resource_principal` | OCI Functions, Container Instances | +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 to build agents with the OpenAI SDK. Only the project OCID is required. ```java -// Session token (local dev) OciAuthConfig config = OciAuthConfig.builder() .authType("security_token") .profile("DEFAULT") - .compartmentId("") .build(); -// Instance principal (OCI Compute) -OciAuthConfig config = OciAuthConfig.builder() - .authType("instance_principal") - .compartmentId("") - .build(); +OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + +OpenAIClient client = new OpenAIClientImpl( + ClientOptions.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") + .apiKey("not-used") + .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) + .build()); ``` -## Endpoint Resolution +## Using Partner APIs (passthrough) -Use `OciEndpointResolver` to derive service URLs from region codes: +Partner endpoints run in pass-through mode and require the compartment OCID header. ```java -import com.oracle.genai.auth.OciEndpointResolver; - -// From region — most common -String url = OciEndpointResolver.resolveBaseUrl("us-chicago-1", null, null, "/20231130/actions/chat"); -// → https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/chat +OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId("ocid1.compartment.oc1..aaaaaaaaexample") + .build(); -// From service endpoint (API path appended) -String url = OciEndpointResolver.resolveBaseUrl(null, - "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - null, "/20231130/actions/chat"); +OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); -// From explicit base URL (used as-is; must be an OCI HTTPS URL) -String url = OciEndpointResolver.resolveBaseUrl(null, null, - "https://custom-endpoint.oci.oraclecloud.com/v1", null); +// The compartment ID is automatically injected as a header by the library +OpenAIClient client = new OpenAIClientImpl( + ClientOptions.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1") + .apiKey("not-used") + .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) + .build()); ``` -Resolution priority: `baseUrl` > `serviceEndpoint` > `region`. +## Authentication Types -## Configuration +| Auth Type | Use Case | +|-----------|----------| +| `oci_config` | Local development with API key in `~/.oci/config` | +| `security_token` | Local development with OCI CLI session token | +| `instance_principal` | OCI Compute instances with dynamic group policies | +| `resource_principal` | OCI Functions, Container Instances | -| Parameter | Description | Required | -|-----------|-------------|----------| -| `authType` | OCI authentication type (see table above) | Yes | -| `profile` | OCI config profile name (default: `DEFAULT`) | No | -| `compartmentId` | OCI compartment OCID | Yes (for GenAI endpoints) | -| `region` | OCI region code (e.g., `us-chicago-1`) | No (for endpoint resolution) | -| `baseUrl` | Fully qualified OCI endpoint override (`*.oraclecloud.com`) | No | -| `timeout` | Request timeout (default: 2 minutes) | No | +## Running the Examples -## Examples +1. Update the constants in each example 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 dependencies: `mvn install -DskipTests`. -The [examples/](examples/) directory contains standalone Java files showing how to use the OCI-signed `OkHttpClient` with different vendor SDKs: +The [examples/](examples/) directory is organized as follows: -| Example | Description | -|---------|-------------| -| [examples/anthropic/](examples/anthropic/) | Anthropic Claude via the `anthropic-java` SDK | -| [examples/openai/](examples/openai/) | OpenAI-compatible models via the `openai-java` SDK | -| [examples/gemini-direct-http/](examples/gemini-direct-http/) | Google Gemini via direct OkHttp POST (no vendor SDK) | +| Directory | Description | +|-----------|-------------| +| [examples/agenthub/openai/](examples/agenthub/openai/) | AgentHub (non-passthrough) examples using the OpenAI Responses API | +| [examples/partner/openai/](examples/partner/openai/) | Partner (passthrough) examples using OpenAI Chat Completions | These examples are **not** compiled as part of the Maven build. Copy them into your own project. -## Module Reference - -| Module | Artifact | Responsibility | -|--------|----------|----------------| -| `oci-genai-auth-java-bom` | `com.oracle.genai:oci-genai-auth-java-bom` | Pins dependency versions | -| `oci-genai-auth-java-core` | `com.oracle.genai:oci-genai-auth-java-core` | OCI IAM auth, request signing, header injection, endpoint resolution | - ## Building from Source ```bash # Compile mvn clean compile -# Run tests (27 tests) +# Run tests mvn test # Full verification @@ -172,12 +191,6 @@ mvn install -DskipTests mvn dependency:tree -pl oci-genai-auth-java-core ``` -## Design Notes - -- **Token refresh** is handled by OCI Java SDK auth providers (`SessionTokenAuthenticationDetailsProvider`, etc.) — no custom refresh logic needed. -- **Spec/codegen** is a separate follow-up track. This library provides auth utilities only. -- **Gemini example** uses direct HTTP because the Google Gemini Java SDK does not currently support transport injection. - ## License Copyright (c) 2026 Oracle and/or its affiliates. diff --git a/examples/agenthub/openai/QuickstartResponsesApiKey.java b/examples/agenthub/openai/QuickstartResponsesApiKey.java new file mode 100644 index 0000000..9c9851c --- /dev/null +++ b/examples/agenthub/openai/QuickstartResponsesApiKey.java @@ -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/ + */ + +/** + * Quickstart using Generative AI API Key authentication with the Responses API. + * + *

    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. + *
    3. Create a Generative AI API Key on OCI Console
    4. + *
    5. Set OPENAI_API_KEY environment variable
    6. + *
    7. Run this example
    8. + *
    + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; + +public class QuickstartResponsesApiKey { + + // ── Configuration ────────────────────────────────────────────────── + private static final String PROJECT_OCID = "<>"; + // ──────────────────────────────────────────────────────────────────── + + public static void main(String[] args) { + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") + .apiKey(System.getenv("OPENAI_API_KEY")) + .build(); + + Response response = client.responses().create( + ResponseCreateParams.builder() + .model("xai.grok-3") + .input("What is 2x2?") + .build()); + + System.out.println(response.outputText()); + } +} diff --git a/examples/agenthub/openai/QuickstartResponsesOciIam.java b/examples/agenthub/openai/QuickstartResponsesOciIam.java new file mode 100644 index 0000000..7fece0f --- /dev/null +++ b/examples/agenthub/openai/QuickstartResponsesOciIam.java @@ -0,0 +1,61 @@ +/* + * 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 with the Responses API on AgentHub. + * + *

    Steps: + *

      + *
    1. Create a Generative AI Project on OCI Console
    2. + *
    3. Add oci-genai-auth-java-core dependency
    4. + *
    5. Run this example
    6. + *
    + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class QuickstartResponsesOciIam { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String PROJECT_OCID = "<>"; + private static final String MODEL = "xai.grok-3"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/openai/v1"; + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + Response response = client.responses().create( + ResponseCreateParams.builder() + .model(MODEL) + .input("What is 2x2?") + .build()); + + System.out.println(response.outputText()); + } +} diff --git a/examples/agenthub/openai/responses/CreateResponse.java b/examples/agenthub/openai/responses/CreateResponse.java new file mode 100644 index 0000000..ff0499d --- /dev/null +++ b/examples/agenthub/openai/responses/CreateResponse.java @@ -0,0 +1,54 @@ +/* + * 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. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class CreateResponse { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String PROJECT_OCID = "<>"; + private static final String MODEL = "xai.grok-3"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/openai/v1"; + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + Response response = client.responses().create( + ResponseCreateParams.builder() + .model(MODEL) + .input("What is 2x2?") + .build()); + + System.out.println(response.outputText()); + } +} diff --git a/examples/agenthub/openai/responses/StreamingTextDelta.java b/examples/agenthub/openai/responses/StreamingTextDelta.java new file mode 100644 index 0000000..637f58a --- /dev/null +++ b/examples/agenthub/openai/responses/StreamingTextDelta.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.ResponseCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class StreamingTextDelta { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String PROJECT_OCID = "<>"; + private static final String MODEL = "xai.grok-3"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/openai/v1"; + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + client.responses().createStreaming( + ResponseCreateParams.builder() + .model(MODEL) + .input("What are the shapes of OCI GPUs?") + .build()) + .stream() + .forEach(event -> { + event.asResponseOutputTextDeltaEvent().ifPresent(delta -> + System.out.print(delta.delta())); + }); + + System.out.println(); + } +} diff --git a/examples/agenthub/openai/tools/FunctionCalling.java b/examples/agenthub/openai/tools/FunctionCalling.java new file mode 100644 index 0000000..da06d63 --- /dev/null +++ b/examples/agenthub/openai/tools/FunctionCalling.java @@ -0,0 +1,105 @@ +/* + * 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 using the Responses API. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.*; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +import java.util.List; +import java.util.Map; + +public class FunctionCalling { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String PROJECT_OCID = "<>"; + private static final String MODEL = "xai.grok-3"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/openai/v1"; + + /** Mock weather function. */ + private static String getCurrentWeather(String location) { + return """ + {"location": "%s", "temperature": "72", "unit": "fahrenheit", "forecast": ["sunny", "windy"]} + """.formatted(location).trim(); + } + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + // Define function tool + Tool weatherTool = Tool.ofFunction(FunctionTool.builder() + .name("get_current_weather") + .description("Get current weather for a given location.") + .strict(true) + .parameters(FunctionTool.Parameters.builder() + .putAdditionalProperty("type", "object") + .putAdditionalProperty("properties", Map.of( + "location", Map.of( + "type", "string", + "description", "City and country e.g. Bogota, Colombia"))) + .putAdditionalProperty("required", List.of("location")) + .putAdditionalProperty("additionalProperties", false) + .build()) + .build()); + + // First request — model decides to call the function + Response response = client.responses().create( + ResponseCreateParams.builder() + .model(MODEL) + .input("What is the weather in Seattle?") + .addTool(weatherTool) + .build()); + + System.out.println("First response: " + response.output()); + + // If the model requested a function call, execute it and send the result back + for (ResponseOutputItem item : response.output()) { + item.functionCall().ifPresent(fc -> { + String result = getCurrentWeather("Seattle"); + + Response followUp = client.responses().create( + ResponseCreateParams.builder() + .model(MODEL) + .addInputItem(ResponseInputItem.ofFunctionCallOutput( + ResponseInputItem.FunctionCallOutput.builder() + .callId(fc.callId()) + .output(result) + .build())) + .addTool(weatherTool) + .build()); + + followUp.output().forEach(out -> + out.message().ifPresent(msg -> + msg.content().forEach(c -> + c.outputText().ifPresent(t -> + System.out.println("Final response: " + t.text()))))); + }); + } + } +} diff --git a/examples/agenthub/openai/tools/WebSearch.java b/examples/agenthub/openai/tools/WebSearch.java new file mode 100644 index 0000000..dff6126 --- /dev/null +++ b/examples/agenthub/openai/tools/WebSearch.java @@ -0,0 +1,57 @@ +/* + * 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. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.Tool; +import com.openai.models.responses.WebSearchTool; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class WebSearch { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String PROJECT_OCID = "<>"; + private static final String MODEL = "openai.gpt-4.1"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/openai/v1"; + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + Response response = client.responses().create( + ResponseCreateParams.builder() + .model(MODEL) + .addTool(Tool.ofWebSearch(WebSearchTool.builder().build())) + .input("What was a positive news story on 2025-11-14?") + .build()); + + System.out.println(response.outputText()); + } +} diff --git a/examples/anthropic/OciAnthropicExample.java b/examples/anthropic/OciAnthropicExample.java deleted file mode 100644 index 8f8b441..0000000 --- a/examples/anthropic/OciAnthropicExample.java +++ /dev/null @@ -1,93 +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/ - */ - -/** - * Example: Using the Anthropic Java SDK with OCI GenAI via oci-genai-auth-java-core. - * - *

    This file is a standalone example — it is NOT compiled as part of the Maven build. - * Copy it into your own project and add the required dependencies (see below). - * - *

    Dependencies (Maven)

    - *
    {@code
    - * 
    - *     com.oracle.genai
    - *     oci-genai-auth-java-core
    - *     0.1.0-SNAPSHOT
    - * 
    - * 
    - *     com.anthropic
    - *     anthropic-java
    - *     1.2.0
    - * 
    - * }
    - * - *

    Run

    - *
    - * javac -cp "lib/*" OciAnthropicExample.java
    - * java  -cp "lib/*:." OciAnthropicExample
    - * 
    - */ - -import com.anthropic.client.AnthropicClient; -import com.anthropic.client.okhttp.AnthropicOkHttpClient; -import com.anthropic.models.messages.MessageCreateParams; -import com.anthropic.models.messages.ContentBlock; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.Model; - -import com.oracle.genai.auth.OciAuthConfig; -import com.oracle.genai.auth.OciEndpointResolver; -import com.oracle.genai.auth.OciOkHttpClientFactory; - -import okhttp3.OkHttpClient; - -public class OciAnthropicExample { - - // ── Configuration ────────────────────────────────────────────────── - private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal - private static final String PROFILE = "DEFAULT"; - private static final String REGION = "us-chicago-1"; - private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; - private static final String API_PATH = "/20231130/actions/chat"; - // ──────────────────────────────────────────────────────────────────── - - public static void main(String[] args) { - // 1. Build an OCI-signed OkHttpClient - OciAuthConfig config = OciAuthConfig.builder() - .authType(AUTH_TYPE) - .profile(PROFILE) - .compartmentId(COMPARTMENT_ID) - .build(); - - OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - - // 2. Resolve the OCI GenAI endpoint for Anthropic - String baseUrl = OciEndpointResolver.resolveBaseUrl( - REGION, null, null, API_PATH); - - // 3. Plug the OCI-signed client into the Anthropic SDK - // The Anthropic SDK expects an HTTP transport — we provide the signed OkHttpClient. - AnthropicClient anthropicClient = AnthropicOkHttpClient.builder() - .baseUrl(baseUrl) - .okHttpClient(ociHttpClient) - .apiKey("OCI_AUTH") // placeholder; OCI signing replaces API key auth - .build(); - - // 4. Send a chat completion request - Message message = anthropicClient.messages().create( - MessageCreateParams.builder() - .model(Model.CLAUDE_HAIKU_4_5_20251001) - .maxTokens(256) - .addUserMessage("What is the capital of France? Answer in one sentence.") - .build()); - - // 5. Print the response - for (ContentBlock block : message.content()) { - block.text().ifPresent(textBlock -> - System.out.println("Response: " + textBlock.text())); - } - } -} diff --git a/examples/gemini-direct-http/OciGeminiDirectExample.java b/examples/gemini-direct-http/OciGeminiDirectExample.java deleted file mode 100644 index 48ed80e..0000000 --- a/examples/gemini-direct-http/OciGeminiDirectExample.java +++ /dev/null @@ -1,102 +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/ - */ - -/** - * Example: Calling Google Gemini via OCI GenAI using direct HTTP (no vendor SDK). - * - *

    This demonstrates using oci-genai-auth-java-core with raw OkHttp when a vendor - * SDK does not support transport injection. The OCI-signed OkHttpClient handles - * authentication transparently — you just build and send requests. - * - *

    This file is a standalone example — it is NOT compiled as part of the Maven build. - * Copy it into your own project and add the required dependencies (see below). - * - *

    Dependencies (Maven)

    - *
    {@code
    - * 
    - *     com.oracle.genai
    - *     oci-genai-auth-java-core
    - *     0.1.0-SNAPSHOT
    - * 
    - * }
    - * - *

    Run

    - *
    - * javac -cp "lib/*" OciGeminiDirectExample.java
    - * java  -cp "lib/*:." OciGeminiDirectExample
    - * 
    - */ - -import com.oracle.genai.auth.OciAuthConfig; -import com.oracle.genai.auth.OciEndpointResolver; -import com.oracle.genai.auth.OciOkHttpClientFactory; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import java.io.IOException; - -public class OciGeminiDirectExample { - - // ── Configuration ────────────────────────────────────────────────── - private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal - private static final String PROFILE = "DEFAULT"; - private static final String REGION = "us-chicago-1"; - private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; - private static final String API_PATH = "/google"; - private static final String MODEL = "google.gemini-2.5-flash"; - // ──────────────────────────────────────────────────────────────────── - - private static final MediaType JSON = MediaType.parse("application/json"); - - public static void main(String[] args) throws IOException { - // 1. Build an OCI-signed OkHttpClient - OciAuthConfig config = OciAuthConfig.builder() - .authType(AUTH_TYPE) - .profile(PROFILE) - .compartmentId(COMPARTMENT_ID) - .build(); - - OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - - // 2. Resolve the OCI GenAI endpoint for Google Gemini - String baseUrl = OciEndpointResolver.resolveBaseUrl( - REGION, null, null, API_PATH); - String url = baseUrl + "/v1beta/models/" + MODEL + ":generateContent"; - - // 3. Build the request JSON (Google Gemini generateContent format) - String requestJson = """ - { - "contents": [ - { - "role": "user", - "parts": [ - { - "text": "What is the capital of France? Answer in one sentence." - } - ] - } - ] - } - """; - - // 4. Send the request using the OCI-signed OkHttpClient - Request request = new Request.Builder() - .url(url) - .post(RequestBody.create(requestJson, JSON)) - .build(); - - try (Response response = ociHttpClient.newCall(request).execute()) { - System.out.println("Status: " + response.code()); - if (response.body() != null) { - System.out.println("Response: " + response.body().string()); - } - } - } -} diff --git a/examples/openai/OciOpenAIExample.java b/examples/openai/OciOpenAIExample.java deleted file mode 100644 index 7ab7bdf..0000000 --- a/examples/openai/OciOpenAIExample.java +++ /dev/null @@ -1,88 +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/ - */ - -/** - * Example: Using the OpenAI Java SDK with OCI GenAI via oci-genai-auth-java-core. - * - *

    This file is a standalone example — it is NOT compiled as part of the Maven build. - * Copy it into your own project and add the required dependencies (see below). - * - *

    Dependencies (Maven)

    - *
    {@code
    - * 
    - *     com.oracle.genai
    - *     oci-genai-auth-java-core
    - *     0.1.0-SNAPSHOT
    - * 
    - * 
    - *     com.openai
    - *     openai-java
    - *     0.34.1
    - * 
    - * }
    - * - *

    Run

    - *
    - * javac -cp "lib/*" OciOpenAIExample.java
    - * java  -cp "lib/*:." OciOpenAIExample
    - * 
    - */ - -import com.openai.client.OpenAIClient; -import com.openai.client.okhttp.OpenAIOkHttpClient; -import com.openai.models.chat.completions.ChatCompletion; -import com.openai.models.chat.completions.ChatCompletionCreateParams; - -import com.oracle.genai.auth.OciAuthConfig; -import com.oracle.genai.auth.OciEndpointResolver; -import com.oracle.genai.auth.OciOkHttpClientFactory; - -import okhttp3.OkHttpClient; - -public class OciOpenAIExample { - - // ── Configuration ────────────────────────────────────────────────── - private static final String AUTH_TYPE = "security_token"; // or oci_config, instance_principal, resource_principal - private static final String PROFILE = "DEFAULT"; - private static final String REGION = "us-chicago-1"; - private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..YOUR_COMPARTMENT_ID"; - private static final String API_PATH = "/20231130/actions/chat"; - // ──────────────────────────────────────────────────────────────────── - - public static void main(String[] args) { - // 1. Build an OCI-signed OkHttpClient - OciAuthConfig config = OciAuthConfig.builder() - .authType(AUTH_TYPE) - .profile(PROFILE) - .compartmentId(COMPARTMENT_ID) - .build(); - - OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - - // 2. Resolve the OCI GenAI endpoint for OpenAI-compatible API - String baseUrl = OciEndpointResolver.resolveBaseUrl( - REGION, null, null, API_PATH); - - // 3. Plug the OCI-signed client into the OpenAI SDK - OpenAIClient openAIClient = OpenAIOkHttpClient.builder() - .baseUrl(baseUrl) - .okHttpClient(ociHttpClient) - .apiKey("OCI_AUTH") // placeholder; OCI signing replaces API key auth - .build(); - - // 4. Send a chat completion request - ChatCompletion completion = openAIClient.chat().completions().create( - ChatCompletionCreateParams.builder() - .model("meta.llama-3.1-405b-instruct") - .addUserMessage("What is the capital of France? Answer in one sentence.") - .build()); - - // 5. Print the response - completion.choices().forEach(choice -> - choice.message().content().ifPresent(content -> - System.out.println("Response: " + content))); - } -} diff --git a/examples/partner/openai/BasicChatCompletion.java b/examples/partner/openai/BasicChatCompletion.java new file mode 100644 index 0000000..633c515 --- /dev/null +++ b/examples/partner/openai/BasicChatCompletion.java @@ -0,0 +1,65 @@ +/* + * 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. + * + *

    This file is a standalone example — it is NOT compiled as part of the Maven build. + * Copy it into your own project and add the required dependencies. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class BasicChatCompletion { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "<>"; + private static final String MODEL = "openai.gpt-4.1"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + + public static void main(String[] args) { + // 1. Build an OCI-signed OkHttpClient + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + // 2. Plug the OCI-signed client into the OpenAI SDK + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + // 3. Send a chat completion request + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model(MODEL) + .addSystemMessage("You are a concise assistant.") + .addUserMessage("List three creative uses for a paperclip.") + .maxTokens(128) + .build()); + + // 4. Print the response + completion.choices().forEach(choice -> + choice.message().content().ifPresent(System.out::println)); + } +} diff --git a/examples/partner/openai/BasicChatCompletionApiKey.java b/examples/partner/openai/BasicChatCompletionApiKey.java new file mode 100644 index 0000000..43cb0d5 --- /dev/null +++ b/examples/partner/openai/BasicChatCompletionApiKey.java @@ -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 chat completion using OCI Generative AI API Key authentication. + * + *

    No oci-genai-auth package needed for API Key auth — just the official OpenAI SDK. + * + *

    Steps: + *

      + *
    1. Create API keys in Console: Generative AI → API Keys
    2. + *
    3. Set OPENAI_API_KEY environment variable
    4. + *
    5. Run this example
    6. + *
    + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; + +public class BasicChatCompletionApiKey { + + private static final String MODEL = "openai.gpt-4.1"; + + public static void main(String[] args) { + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") + .apiKey(System.getenv("OPENAI_API_KEY")) + .build(); + + ChatCompletion completion = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model(MODEL) + .addSystemMessage("You are a concise assistant who answers in one paragraph.") + .addUserMessage("Explain why the sky is blue as if you were a physics teacher.") + .build()); + + completion.choices().forEach(choice -> + choice.message().content().ifPresent(System.out::println)); + } +} diff --git a/examples/partner/openai/StreamingChatCompletion.java b/examples/partner/openai/StreamingChatCompletion.java new file mode 100644 index 0000000..8e5f20e --- /dev/null +++ b/examples/partner/openai/StreamingChatCompletion.java @@ -0,0 +1,61 @@ +/* + * 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. + */ + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +public class StreamingChatCompletion { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "<>"; + private static final String MODEL = "openai.gpt-4.1"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + // Stream the response + client.chat().completions().createStreaming( + ChatCompletionCreateParams.builder() + .model(MODEL) + .addSystemMessage("You are a concise assistant who answers in one paragraph.") + .addUserMessage("Explain why the sky is blue as if you were a physics teacher.") + .build()) + .stream() + .flatMap(chunk -> chunk.choices().stream()) + .forEach(choice -> choice.delta().content().ifPresent( + content -> System.out.print(content))); + + System.out.println(); + } +} diff --git a/examples/partner/openai/ToolCallChatCompletion.java b/examples/partner/openai/ToolCallChatCompletion.java new file mode 100644 index 0000000..6faeb96 --- /dev/null +++ b/examples/partner/openai/ToolCallChatCompletion.java @@ -0,0 +1,113 @@ +/* + * 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 com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.chat.completions.*; + +import com.oracle.genai.auth.OciAuthConfig; +import com.oracle.genai.auth.OciOkHttpClientFactory; + +import okhttp3.OkHttpClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ToolCallChatCompletion { + + // ── Configuration ────────────────────────────────────────────────── + private static final String REGION = "us-chicago-1"; + private static final String COMPARTMENT_ID = "<>"; + private static final String MODEL = "openai.gpt-4.1"; + // ──────────────────────────────────────────────────────────────────── + + private static final String BASE_URL = + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + + /** Mock weather function. */ + private static String getCurrentWeather(String location) { + return """ + {"location": "%s", "temperature": "72", "unit": "fahrenheit", "forecast": ["sunny", "windy"]} + """.formatted(location).trim(); + } + + public static void main(String[] args) { + OciAuthConfig config = OciAuthConfig.builder() + .authType("security_token") + .profile("DEFAULT") + .compartmentId(COMPARTMENT_ID) + .build(); + + OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + + OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl(BASE_URL) + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .build(); + + // Define the function tool + ChatCompletionTool weatherTool = ChatCompletionTool.builder() + .function(FunctionDefinition.builder() + .name("get_current_weather") + .description("Get the current weather for a specific location.") + .parameters(ChatCompletionTool.Function.Parameters.builder() + .putAdditionalProperty("type", "object") + .putAdditionalProperty("properties", Map.of( + "location", Map.of( + "type", "string", + "description", "City and state, for example Boston, MA."))) + .putAdditionalProperty("required", List.of("location")) + .build()) + .build()) + .build(); + + // First request + ChatCompletion first = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model(MODEL) + .addUserMessage("What is the weather in Boston and San Francisco?") + .addTool(weatherTool) + .toolChoice(ChatCompletionToolChoiceOption.AUTO) + .build()); + + ChatCompletion.Choice firstChoice = first.choices().get(0); + + if ("tool_calls".equals(firstChoice.finishReason().toString())) { + // Execute tool calls and send results back + List messages = new ArrayList<>(); + messages.add(ChatCompletionCreateParams.Message.ofUser( + UserMessage.of("What is the weather in Boston and San Francisco?"))); + messages.add(ChatCompletionCreateParams.Message.ofAssistant( + firstChoice.message())); + + for (var toolCall : firstChoice.message().toolCalls().orElse(List.of())) { + String result = getCurrentWeather(toolCall.function().name()); + messages.add(ChatCompletionCreateParams.Message.ofTool( + ToolMessage.builder() + .toolCallId(toolCall.id()) + .content(result) + .build())); + } + + ChatCompletion followUp = client.chat().completions().create( + ChatCompletionCreateParams.builder() + .model(MODEL) + .messages(messages) + .build()); + + followUp.choices().forEach(choice -> + choice.message().content().ifPresent(System.out::println)); + } else { + firstChoice.message().content().ifPresent(System.out::println); + } + } +} diff --git a/oci-genai-auth-java-core/pom.xml b/oci-genai-auth-java-core/pom.xml index 9626a8d..89d9889 100644 --- a/oci-genai-auth-java-core/pom.xml +++ b/oci-genai-auth-java-core/pom.xml @@ -51,14 +51,6 @@ slf4j-api - - - com.anthropic - anthropic-java - 2.12.0 - test - - com.openai diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java deleted file mode 100644 index 3e51ee0..0000000 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/AnthropicIntegrationTest.java +++ /dev/null @@ -1,231 +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/ - */ -package com.oracle.genai.auth; - -import com.anthropic.client.AnthropicClient; -import com.anthropic.client.AnthropicClientImpl; -import com.anthropic.core.ClientOptions; -import com.anthropic.core.RequestOptions; -import com.anthropic.core.http.HttpClient; -import com.anthropic.core.http.HttpRequest; -import com.anthropic.core.http.HttpRequestBody; -import com.anthropic.core.http.HttpResponse; -import com.anthropic.core.http.Headers; -import com.anthropic.models.messages.ContentBlock; -import com.anthropic.models.messages.Message; -import com.anthropic.models.messages.MessageCreateParams; -import okhttp3.*; -import okio.BufferedSink; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.CompletableFuture; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Integration test: OCI auth library + Anthropic Java SDK against PPE endpoint. - * - *

    Validates that oci-genai-auth-java-core produces a correctly signed - * OkHttpClient that works with the Anthropic SDK. - * - *

    To run: - *

    - * oci session authenticate
    - * mvn -pl oci-genai-auth-java-core test -Dtest=AnthropicIntegrationTest
    - * 
    - */ -class AnthropicIntegrationTest { - - private static final String COMPARTMENT_ID = - System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); - - private static final String BASE_URL = - System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") - + "/anthropic"; - - @Test - @Disabled("Requires live OCI session — run: oci session authenticate") - void anthropic_via_oci_auth_library() { - OciAuthConfig config = OciAuthConfig.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .build(); - - OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - - HttpClient signingHttpClient = new AnthropicOkHttpAdapter(ociHttpClient); - - ClientOptions clientOptions = ClientOptions.builder() - .httpClient(signingHttpClient) - .baseUrl(BASE_URL) - .putHeader("anthropic-version", "2023-06-01") - .build(); - - AnthropicClient client = new AnthropicClientImpl(clientOptions); - - try { - Message message = client.messages().create(MessageCreateParams.builder() - .model("anthropic.claude-haiku-4-5") - .maxTokens(256) - .addUserMessage("What is 2 + 2? Answer in one word.") - .build()); - - assertNotNull(message, "Response should not be null"); - assertFalse(message.content().isEmpty(), "Response should have content"); - - for (ContentBlock block : message.content()) { - block.text().ifPresent(textBlock -> { - System.out.println("Anthropic response: " + textBlock.text()); - assertFalse(textBlock.text().isBlank(), "Response text should not be blank"); - }); - } - } finally { - client.close(); - } - } - - /** - * Adapter: bridges Anthropic SDK's HttpClient to OCI-signed OkHttpClient. - */ - private static class AnthropicOkHttpAdapter implements HttpClient { - - private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json"); - private final OkHttpClient okHttpClient; - - AnthropicOkHttpAdapter(OkHttpClient okHttpClient) { - this.okHttpClient = okHttpClient; - } - - @Override - public HttpResponse execute(HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - try { - Response okResponse = okHttpClient.newCall(okRequest).execute(); - return new OkHttpResponseAdapter(okResponse); - } catch (IOException e) { - throw new RuntimeException("OCI request failed: " + request.url(), e); - } - } - - @Override - public CompletableFuture executeAsync( - HttpRequest request, RequestOptions requestOptions) { - Request okRequest = toOkHttpRequest(request); - CompletableFuture future = new CompletableFuture<>(); - okHttpClient.newCall(okRequest).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - future.completeExceptionally(e); - } - - @Override - public void onResponse(Call call, Response response) { - future.complete(new OkHttpResponseAdapter(response)); - } - }); - return future; - } - - @Override - public void close() { - okHttpClient.dispatcher().executorService().shutdown(); - okHttpClient.connectionPool().evictAll(); - } - - private Request toOkHttpRequest(HttpRequest request) { - HttpUrl parsedUrl = HttpUrl.parse(request.url()); - if (parsedUrl == null) { - throw new IllegalArgumentException("Invalid URL: " + request.url()); - } - - HttpUrl.Builder urlBuilder = parsedUrl.newBuilder(); - var queryParams = request.queryParams(); - for (String key : queryParams.keys()) { - for (String value : queryParams.values(key)) { - urlBuilder.addQueryParameter(key, value); - } - } - - okhttp3.Headers.Builder headersBuilder = new okhttp3.Headers.Builder(); - var headers = request.headers(); - for (String name : headers.names()) { - if ("x-api-key".equalsIgnoreCase(name) || "authorization".equalsIgnoreCase(name)) { - continue; - } - for (String value : headers.values(name)) { - headersBuilder.add(name, value); - } - } - - RequestBody body = null; - HttpRequestBody requestBody = request.body(); - if (requestBody != null) { - body = new RequestBody() { - @Override - public MediaType contentType() { - String ct = requestBody.contentType(); - return ct != null ? MediaType.parse(ct) : JSON_MEDIA_TYPE; - } - - @Override - public long contentLength() { - return requestBody.contentLength(); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - try (OutputStream os = sink.outputStream()) { - requestBody.writeTo(os); - } - } - }; - } - - return new Request.Builder() - .url(urlBuilder.build()) - .headers(headersBuilder.build()) - .method(request.method().name(), body) - .build(); - } - - private static class OkHttpResponseAdapter implements HttpResponse { - private final Response response; - private final Headers headers; - - OkHttpResponseAdapter(Response response) { - this.response = response; - Headers.Builder builder = Headers.builder(); - for (String name : response.headers().names()) { - for (String value : response.headers(name)) { - builder.put(name, value); - } - } - this.headers = builder.build(); - } - - @Override - public int statusCode() { return response.code(); } - - @Override - public Headers headers() { return headers; } - - @Override - public InputStream body() { - ResponseBody b = response.body(); - return b != null ? b.byteStream() : InputStream.nullInputStream(); - } - - @Override - public void close() { response.close(); } - } - } -} diff --git a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java b/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java deleted file mode 100644 index 77151ea..0000000 --- a/oci-genai-auth-java-core/src/test/java/com/oracle/genai/auth/GeminiIntegrationTest.java +++ /dev/null @@ -1,86 +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/ - */ -package com.oracle.genai.auth; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Integration test: OCI auth library + direct HTTP for Gemini via OCI GenAI. - * - *

    Demonstrates that the OCI-signed OkHttpClient works with raw HTTP calls - * (no vendor SDK needed) — useful when a vendor SDK doesn't support transport injection. - * - *

    To run: - *

    - * oci session authenticate
    - * mvn -pl oci-genai-auth-java-core test -Dtest=GeminiIntegrationTest
    - * 
    - */ -class GeminiIntegrationTest { - - private static final String COMPARTMENT_ID = - System.getenv().getOrDefault("OCI_COMPARTMENT_ID", ""); - - private static final String BASE_URL = - System.getenv().getOrDefault("OCI_GENAI_ENDPOINT", - "https://ppe.inference.generativeai.us-chicago-1.oci.oraclecloud.com") - + "/google"; - - private static final MediaType JSON = MediaType.parse("application/json"); - - @Test - @Disabled("Requires live OCI session — run: oci session authenticate") - void gemini_via_direct_http() throws IOException { - OciAuthConfig config = OciAuthConfig.builder() - .authType("security_token") - .profile("DEFAULT") - .compartmentId(COMPARTMENT_ID) - .build(); - - OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); - - String model = "google.gemini-2.5-flash"; - String url = BASE_URL + "/v1beta/models/" + model + ":generateContent"; - - String requestJson = """ - { - "contents": [ - { - "role": "user", - "parts": [ - { - "text": "What is 2 + 2? Answer in one word." - } - ] - } - ] - } - """; - - Request request = new Request.Builder() - .url(url) - .post(RequestBody.create(requestJson, JSON)) - .build(); - - try (Response response = ociHttpClient.newCall(request).execute()) { - String body = response.body() != null ? response.body().string() : ""; - System.out.println("Gemini response (" + response.code() + "): " + body); - - assertNotEquals(401, response.code(), "Auth signing should work"); - assertNotEquals(403, response.code(), "Auth signing should work"); - } - } -} From 4a96366abb6e228f08b87088e2954c4ea1d327cf Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Fri, 20 Mar 2026 10:57:20 -0700 Subject: [PATCH 14/16] Address architect review: update URLs, env vars, and remove passthrough terminology - Change Partner base URL from /v1 to /20231130/actions/v1 - Rename OPENAI_API_KEY to OCI_GENAI_API_KEY across README and examples - Remove "passthrough" / "non-passthrough" language from README - Add examples/agenthub/README.md and examples/partner/openai/README.md Co-Authored-By: Claude Opus 4.6 --- README.md | 26 +++++++------- examples/agenthub/README.md | 35 +++++++++++++++++++ .../openai/QuickstartResponsesApiKey.java | 4 +-- .../partner/openai/BasicChatCompletion.java | 2 +- .../openai/BasicChatCompletionApiKey.java | 2 +- examples/partner/openai/README.md | 31 ++++++++++++++++ .../openai/StreamingChatCompletion.java | 2 +- .../openai/ToolCallChatCompletion.java | 2 +- 8 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 examples/agenthub/README.md create mode 100644 examples/partner/openai/README.md diff --git a/README.md b/README.md index 4a9bf27..c6bc34f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # oci-genai-auth-java -The **OCI GenAI Auth** Java 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. +The **OCI GenAI Auth** Java library provides OCI request-signing helpers for the OpenAI-compatible REST APIs hosted by OCI Generative AI. ## Table of Contents - [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) +- [Using AgentHub APIs](#using-agenthub-apis) +- [Using Partner APIs](#using-partner-apis) - [Running the Examples](#running-the-examples) - [Building from Source](#building-from-source) @@ -84,7 +84,7 @@ OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); // Plug the OCI-signed OkHttpClient into the OpenAI SDK OpenAIClient client = new OpenAIClientImpl( ClientOptions.builder() - .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1") + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") .apiKey("not-used") .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) .build()); @@ -102,13 +102,13 @@ import com.openai.core.ClientOptions; OpenAIClient client = new OpenAIClientImpl( ClientOptions.builder() .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") - .apiKey(System.getenv("OPENAI_API_KEY")) + .apiKey(System.getenv("OCI_GENAI_API_KEY")) .build()); ``` -## Using AgentHub APIs (non-passthrough) +## Using AgentHub APIs -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 to build agents with the OpenAI SDK. Only the project OCID is required. +AgentHub 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 to build agents with the OpenAI SDK. Only the project OCID is required. ```java OciAuthConfig config = OciAuthConfig.builder() @@ -126,9 +126,9 @@ OpenAIClient client = new OpenAIClientImpl( .build()); ``` -## Using Partner APIs (passthrough) +## Using Partner APIs -Partner endpoints run in pass-through mode and require the compartment OCID header. +Partner endpoints require the compartment OCID header. ```java OciAuthConfig config = OciAuthConfig.builder() @@ -142,7 +142,7 @@ OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); // The compartment ID is automatically injected as a header by the library OpenAIClient client = new OpenAIClientImpl( ClientOptions.builder() - .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1") + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/v1") .apiKey("not-used") .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) .build()); @@ -160,15 +160,15 @@ OpenAIClient client = new OpenAIClientImpl( ## Running the Examples 1. Update the constants in each example 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. +2. Set the `OCI_GENAI_API_KEY` environment variable when an example uses API key authentication. 3. Install dependencies: `mvn install -DskipTests`. The [examples/](examples/) directory is organized as follows: | Directory | Description | |-----------|-------------| -| [examples/agenthub/openai/](examples/agenthub/openai/) | AgentHub (non-passthrough) examples using the OpenAI Responses API | -| [examples/partner/openai/](examples/partner/openai/) | Partner (passthrough) examples using OpenAI Chat Completions | +| [examples/agenthub/openai/](examples/agenthub/openai/) | AgentHub examples using the OpenAI Responses API | +| [examples/partner/openai/](examples/partner/openai/) | Partner examples using OpenAI Chat Completions | These examples are **not** compiled as part of the Maven build. Copy them into your own project. diff --git a/examples/agenthub/README.md b/examples/agenthub/README.md new file mode 100644 index 0000000..701e3b1 --- /dev/null +++ b/examples/agenthub/README.md @@ -0,0 +1,35 @@ +# AgentHub Examples + +AgentHub 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 to build agents with the OpenAI SDK. + +## Prerequisites + +1. Create a **Generative AI Project** on OCI Console. +2. Update `PROJECT_OCID` and `REGION` in each example file. +3. Authenticate with OCI: + - **OCI IAM**: `oci session authenticate` + - **API Key**: Set `OCI_GENAI_API_KEY` environment variable + +## Base URL + +``` +https://inference.generativeai..oci.oraclecloud.com/openai/v1 +``` + +## Examples + +| File | Description | +|------|-------------| +| `openai/QuickstartResponsesOciIam.java` | Quickstart with OCI IAM authentication | +| `openai/QuickstartResponsesApiKey.java` | Quickstart with API key authentication | +| `openai/responses/CreateResponse.java` | Create a response | +| `openai/responses/StreamingTextDelta.java` | Stream response text deltas | +| `openai/tools/WebSearch.java` | Web search tool | +| `openai/tools/FunctionCalling.java` | Function calling tool | + +## Running + +These are standalone Java files. Copy them into your project with the required dependencies +(`oci-genai-auth-java-core` and `openai-java`), then compile and run. diff --git a/examples/agenthub/openai/QuickstartResponsesApiKey.java b/examples/agenthub/openai/QuickstartResponsesApiKey.java index 9c9851c..bfda13f 100644 --- a/examples/agenthub/openai/QuickstartResponsesApiKey.java +++ b/examples/agenthub/openai/QuickstartResponsesApiKey.java @@ -13,7 +13,7 @@ *
      *
    1. Create a Generative AI Project on OCI Console
    2. *
    3. Create a Generative AI API Key on OCI Console
    4. - *
    5. Set OPENAI_API_KEY environment variable
    6. + *
    7. Set OCI_GENAI_API_KEY environment variable
    8. *
    9. Run this example
    10. *
    */ @@ -32,7 +32,7 @@ public class QuickstartResponsesApiKey { public static void main(String[] args) { OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") - .apiKey(System.getenv("OPENAI_API_KEY")) + .apiKey(System.getenv("OCI_GENAI_API_KEY")) .build(); Response response = client.responses().create( diff --git a/examples/partner/openai/BasicChatCompletion.java b/examples/partner/openai/BasicChatCompletion.java index 633c515..66293e5 100644 --- a/examples/partner/openai/BasicChatCompletion.java +++ b/examples/partner/openai/BasicChatCompletion.java @@ -30,7 +30,7 @@ public class BasicChatCompletion { // ──────────────────────────────────────────────────────────────────── private static final String BASE_URL = - "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/20231130/actions/v1"; public static void main(String[] args) { // 1. Build an OCI-signed OkHttpClient diff --git a/examples/partner/openai/BasicChatCompletionApiKey.java b/examples/partner/openai/BasicChatCompletionApiKey.java index 43cb0d5..be528ec 100644 --- a/examples/partner/openai/BasicChatCompletionApiKey.java +++ b/examples/partner/openai/BasicChatCompletionApiKey.java @@ -29,7 +29,7 @@ public class BasicChatCompletionApiKey { public static void main(String[] args) { OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") - .apiKey(System.getenv("OPENAI_API_KEY")) + .apiKey(System.getenv("OCI_GENAI_API_KEY")) .build(); ChatCompletion completion = client.chat().completions().create( diff --git a/examples/partner/openai/README.md b/examples/partner/openai/README.md new file mode 100644 index 0000000..9ddf333 --- /dev/null +++ b/examples/partner/openai/README.md @@ -0,0 +1,31 @@ +# Partner Examples + +Partner endpoints provide access to OpenAI, xAI Grok, and Meta Llama models via the +OpenAI Chat Completions API. The compartment OCID header is required. + +## Prerequisites + +1. Update `COMPARTMENT_ID` and `REGION` in each example file. +2. Authenticate with OCI: + - **OCI IAM**: `oci session authenticate` + - **API Key**: Set `OCI_GENAI_API_KEY` environment variable + +## Base URL + +``` +https://inference.generativeai..oci.oraclecloud.com/20231130/actions/v1 +``` + +## Examples + +| File | Description | +|------|-------------| +| `BasicChatCompletion.java` | Basic chat completion with OCI IAM auth | +| `BasicChatCompletionApiKey.java` | Chat completion with API key auth | +| `StreamingChatCompletion.java` | Streaming chat completion | +| `ToolCallChatCompletion.java` | Function/tool calling with chat completions | + +## Running + +These are standalone Java files. Copy them into your project with the required dependencies +(`oci-genai-auth-java-core` and `openai-java`), then compile and run. diff --git a/examples/partner/openai/StreamingChatCompletion.java b/examples/partner/openai/StreamingChatCompletion.java index 8e5f20e..38ca1bf 100644 --- a/examples/partner/openai/StreamingChatCompletion.java +++ b/examples/partner/openai/StreamingChatCompletion.java @@ -27,7 +27,7 @@ public class StreamingChatCompletion { // ──────────────────────────────────────────────────────────────────── private static final String BASE_URL = - "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/20231130/actions/v1"; public static void main(String[] args) { OciAuthConfig config = OciAuthConfig.builder() diff --git a/examples/partner/openai/ToolCallChatCompletion.java b/examples/partner/openai/ToolCallChatCompletion.java index 6faeb96..5262635 100644 --- a/examples/partner/openai/ToolCallChatCompletion.java +++ b/examples/partner/openai/ToolCallChatCompletion.java @@ -30,7 +30,7 @@ public class ToolCallChatCompletion { // ──────────────────────────────────────────────────────────────────── private static final String BASE_URL = - "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/v1"; + "https://inference.generativeai." + REGION + ".oci.oraclecloud.com/20231130/actions/v1"; /** Mock weather function. */ private static String getCurrentWeather(String location) { From f0c18caa2508edacbafa0efc99c56d902097ce44 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Fri, 20 Mar 2026 11:11:12 -0700 Subject: [PATCH 15/16] Add Contributing and Security sections to README for OGHO compliance OGHO requires README.md to explicitly link to CONTRIBUTING.md and SECURITY.md. Co-Authored-By: Claude Opus 4.6 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index c6bc34f..ba8562d 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,14 @@ mvn install -DskipTests mvn dependency:tree -pl oci-genai-auth-java-core ``` +## 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. From d010c95223e7eaf6c84d0e0caa8619c3dee34111 Mon Sep 17 00:00:00 2001 From: Junhui Li Date: Fri, 20 Mar 2026 14:39:01 -0700 Subject: [PATCH 16/16] =?UTF-8?q?Add=20project=20OCID=20to=20AgentHub=20ex?= =?UTF-8?q?amples=20=E2=80=94=20no=20compartment=20ID=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentHub DP endpoints only require the project_id (via openai-project header), not the compartment_id. Updated all AgentHub examples and README to reflect this. Co-Authored-By: Claude Opus 4.6 --- README.md | 14 +++++++------- .../agenthub/openai/QuickstartResponsesApiKey.java | 2 ++ .../agenthub/openai/QuickstartResponsesOciIam.java | 2 ++ .../agenthub/openai/responses/CreateResponse.java | 2 ++ .../openai/responses/StreamingTextDelta.java | 2 ++ .../agenthub/openai/tools/FunctionCalling.java | 2 ++ examples/agenthub/openai/tools/WebSearch.java | 2 ++ 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ba8562d..7228551 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ OpenAIClient client = new OpenAIClientImpl( ## Using AgentHub APIs -AgentHub 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 to build agents with the OpenAI SDK. Only the project OCID is required. +AgentHub 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 to build agents with the OpenAI SDK. Only the project OCID is required — no compartment ID needed. ```java OciAuthConfig config = OciAuthConfig.builder() @@ -118,12 +118,12 @@ OciAuthConfig config = OciAuthConfig.builder() OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); -OpenAIClient client = new OpenAIClientImpl( - ClientOptions.builder() - .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") - .apiKey("not-used") - .httpClient(new OpenAIOkHttpAdapter(ociHttpClient, baseUrl)) - .build()); +OpenAIClient client = OpenAIOkHttpClient.builder() + .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") + .okHttpClient(ociHttpClient) + .apiKey("not-used") + .addHeader("openai-project", "ocid1.generativeaiproject.oc1.us-chicago-1.aaaaaaaaexample") + .build(); ``` ## Using Partner APIs diff --git a/examples/agenthub/openai/QuickstartResponsesApiKey.java b/examples/agenthub/openai/QuickstartResponsesApiKey.java index bfda13f..283df65 100644 --- a/examples/agenthub/openai/QuickstartResponsesApiKey.java +++ b/examples/agenthub/openai/QuickstartResponsesApiKey.java @@ -30,9 +30,11 @@ public class QuickstartResponsesApiKey { // ──────────────────────────────────────────────────────────────────── public static void main(String[] args) { + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl("https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1") .apiKey(System.getenv("OCI_GENAI_API_KEY")) + .addHeader("openai-project", PROJECT_OCID) .build(); Response response = client.responses().create( diff --git a/examples/agenthub/openai/QuickstartResponsesOciIam.java b/examples/agenthub/openai/QuickstartResponsesOciIam.java index 7fece0f..634f542 100644 --- a/examples/agenthub/openai/QuickstartResponsesOciIam.java +++ b/examples/agenthub/openai/QuickstartResponsesOciIam.java @@ -44,10 +44,12 @@ public static void main(String[] args) { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl(BASE_URL) .okHttpClient(ociHttpClient) .apiKey("not-used") + .addHeader("openai-project", PROJECT_OCID) .build(); Response response = client.responses().create( diff --git a/examples/agenthub/openai/responses/CreateResponse.java b/examples/agenthub/openai/responses/CreateResponse.java index ff0499d..7986335 100644 --- a/examples/agenthub/openai/responses/CreateResponse.java +++ b/examples/agenthub/openai/responses/CreateResponse.java @@ -37,10 +37,12 @@ public static void main(String[] args) { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl(BASE_URL) .okHttpClient(ociHttpClient) .apiKey("not-used") + .addHeader("openai-project", PROJECT_OCID) .build(); Response response = client.responses().create( diff --git a/examples/agenthub/openai/responses/StreamingTextDelta.java b/examples/agenthub/openai/responses/StreamingTextDelta.java index 637f58a..33e571a 100644 --- a/examples/agenthub/openai/responses/StreamingTextDelta.java +++ b/examples/agenthub/openai/responses/StreamingTextDelta.java @@ -36,10 +36,12 @@ public static void main(String[] args) { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl(BASE_URL) .okHttpClient(ociHttpClient) .apiKey("not-used") + .addHeader("openai-project", PROJECT_OCID) .build(); client.responses().createStreaming( diff --git a/examples/agenthub/openai/tools/FunctionCalling.java b/examples/agenthub/openai/tools/FunctionCalling.java index da06d63..4d38cdf 100644 --- a/examples/agenthub/openai/tools/FunctionCalling.java +++ b/examples/agenthub/openai/tools/FunctionCalling.java @@ -46,10 +46,12 @@ public static void main(String[] args) { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl(BASE_URL) .okHttpClient(ociHttpClient) .apiKey("not-used") + .addHeader("openai-project", PROJECT_OCID) .build(); // Define function tool diff --git a/examples/agenthub/openai/tools/WebSearch.java b/examples/agenthub/openai/tools/WebSearch.java index dff6126..c70864d 100644 --- a/examples/agenthub/openai/tools/WebSearch.java +++ b/examples/agenthub/openai/tools/WebSearch.java @@ -39,10 +39,12 @@ public static void main(String[] args) { OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config); + // AgentHub only needs project OCID — no compartment ID required OpenAIClient client = OpenAIOkHttpClient.builder() .baseUrl(BASE_URL) .okHttpClient(ociHttpClient) .apiKey("not-used") + .addHeader("openai-project", PROJECT_OCID) .build(); Response response = client.responses().create(