diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..ec6b35179 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,90 @@ +include: + - local: '.gitlab/ci/go-private-modules.yml' + +stages: + - sync + - maintain + - build + - test + +variables: + GO_VERSION: "1.24" + +# Renovatebot dependency updates +renovate: + stage: maintain + image: renovate/renovate:latest + script: + - renovate --platform=gitlab --token=$RENOVATE_TOKEN + variables: + RENOVATE_PLATFORM: gitlab + RENOVATE_ENDPOINT: https://gitlab.com/api/v4 + RENOVATE_AUTODISCOVER: "false" + RENOVATE_BASE_DIR: $CI_PROJECT_DIR + LOG_LEVEL: debug + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "renovate" + when: always + +# Sync upstream main branch from charmbracelet/fantasy +sync-upstream-main: + stage: sync + image: alpine/git:latest + before_script: + - git config --global user.name "GitLab CI" + - git config --global user.email "ci@tinyland.ai" + - git remote add charmbracelet https://github.com/charmbracelet/fantasy.git || true + script: + - git fetch charmbracelet main + - SYNC_DATE=$(date +%Y%m%d-%H%M) + - BRANCH_NAME="sync/upstream-main-$SYNC_DATE" + - git checkout -b $BRANCH_NAME + - git merge charmbracelet/main --no-edit || (echo "Merge conflict detected" && exit 1) + - git push https://oauth2:${GITLAB_TOKEN}@gitlab.com/${CI_PROJECT_PATH}.git $BRANCH_NAME + - | + curl --request POST \ + --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{ + \"source_branch\": \"$BRANCH_NAME\", + \"target_branch\": \"main\", + \"title\": \"chore: Sync upstream main ($SYNC_DATE)\", + \"description\": \"🔄 Automated sync from charmbracelet/fantasy main branch\\n\\n**Review Checklist:**\\n- [ ] Check for breaking API changes\\n- [ ] Validate Go version compatibility\\n- [ ] Review new providers or features\\n- [ ] Test downstream compatibility (Crush)\", + \"labels\": \"upstream-sync,charmbracelet\" + }" \ + "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests" + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "sync-main" + when: always + - when: manual + allow_failure: true + +# Build job +build: + extends: .go_private_modules_setup + stage: build + image: golang:${GO_VERSION} + script: + - go mod download + - go build -v ./... + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == "main" + +# Test job +test: + extends: .go_private_modules_setup + stage: test + image: golang:${GO_VERSION} + script: + - go mod download + - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + coverage: '/coverage: \d+.\d+% of statements/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.txt + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == "main" diff --git a/.gitlab/ci/go-private-modules.yml b/.gitlab/ci/go-private-modules.yml new file mode 100644 index 000000000..0b560056e --- /dev/null +++ b/.gitlab/ci/go-private-modules.yml @@ -0,0 +1,174 @@ +# GitLab CI Configuration for Go Projects with Private Modules +# ============================================================= +# +# This template configures Go builds to access private GitLab modules +# from the tinyland namespace. +# +# Add this to your .gitlab-ci.yml: +# +# include: +# - local: '.gitlab/ci/go-private-modules.yml' + +# Variables for Go private module access +variables: + # Configure Go to treat tinyland modules as private + GOPRIVATE: "gitlab.com/tinyland/*" + GONOSUMDB: "gitlab.com/tinyland/*" + GONOPROXY: "gitlab.com/tinyland/*" + + # Use Go module proxy for public modules + GOPROXY: "https://proxy.golang.org,direct" + +# Before script template for Go jobs needing private module access +.go_private_modules_setup: + before_script: + - | + echo "Setting up Go private module access..." + + # Configure Git to use GitLab CI token for private repos + git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/" + + # Alternative: Use project/group access token if CI_JOB_TOKEN doesn't have access + # Uncomment and set GITLAB_PRIVATE_TOKEN in CI/CD variables: + # git config --global url."https://oauth2:${GITLAB_PRIVATE_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/" + + # Configure netrc for Go module downloads + cat > ~/.netrc < Access Tokens +# 2. Scopes: read_repository, read_api +# 3. Add to CI/CD variables: GITLAB_PRIVATE_TOKEN +# 4. Uncomment the alternative git config line in .go_private_modules_setup + +# Example: SSH-based authentication +# ---------------------------------- +# If you prefer SSH over HTTPS: +# +# 1. Add deploy key to private module repos +# 2. Add private key to CI/CD variables: SSH_PRIVATE_KEY +# 3. Use this before_script instead: +# +# .go_private_modules_ssh_setup: +# before_script: +# - | +# eval $(ssh-agent -s) +# echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - +# mkdir -p ~/.ssh +# chmod 700 ~/.ssh +# ssh-keyscan gitlab.com >> ~/.ssh/known_hosts +# git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/" diff --git a/.gitlab/renovate.json b/.gitlab/renovate.json new file mode 100644 index 000000000..a034e91c3 --- /dev/null +++ b/.gitlab/renovate.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "platformAutomerge": false, + "baseBranches": ["main"], + "labels": ["dependencies", "renovate"], + "assignees": ["@jesssullivan"], + "reviewers": ["@jesssullivan"], + "timezone": "America/New_York", + "schedule": ["before 5am on monday"], + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "dependencyDashboard": true, + "dependencyDashboardTitle": "🤖 Dependency Updates Dashboard", + "dependencyDashboardHeader": "This issue lists all pending dependency updates for Fantasy SDK institutional fork.", + "dependencyDashboardLabels": ["dependencies", "renovate-dashboard"], + "packageRules": [ + { + "description": "Charmbracelet ecosystem packages", + "matchPackagePatterns": [ + "^github.com/charmbracelet/" + ], + "labels": ["charmbracelet", "ecosystem"], + "groupName": "charmbracelet packages", + "automerge": false, + "commitMessagePrefix": "chore(deps):", + "semanticCommitType": "chore", + "semanticCommitScope": "deps" + }, + { + "description": "OpenAI SDK updates", + "matchPackagePatterns": ["github.com/openai/openai-go"], + "labels": ["openai", "sdk", "provider"], + "automerge": false, + "minimumReleaseAge": "3 days", + "prBodyNotes": ["🔍 Test provider compatibility after upgrade"] + }, + { + "description": "Anthropic SDK updates", + "matchPackagePatterns": ["github.com/charmbracelet/anthropic-sdk-go"], + "labels": ["anthropic", "sdk", "provider"], + "automerge": false, + "minimumReleaseAge": "3 days", + "prBodyNotes": ["🔍 Test provider compatibility after upgrade"] + }, + { + "description": "AWS SDK updates", + "matchPackagePatterns": ["^github.com/aws/"], + "labels": ["aws", "sdk", "provider"], + "groupName": "aws sdk", + "automerge": false + }, + { + "description": "Google Cloud SDK updates", + "matchPackagePatterns": ["^cloud.google.com/"], + "labels": ["gcp", "sdk", "provider"], + "groupName": "gcp sdk", + "automerge": false + }, + { + "description": "Go module digest updates - auto-merge when CI passes", + "matchDatasources": ["go"], + "matchUpdateTypes": ["digest"], + "labels": ["golang", "digest-update", "auto-merge"], + "automerge": true, + "commitMessagePrefix": "chore(deps):", + "semanticCommitType": "chore", + "semanticCommitScope": "deps" + }, + { + "description": "Go module minor and patch updates - auto-merge when CI passes", + "matchDatasources": ["go"], + "matchUpdateTypes": ["minor", "patch"], + "labels": ["golang", "minor-update", "auto-merge"], + "groupName": "go dependencies (non-major)", + "rangeStrategy": "bump", + "automerge": true + }, + { + "description": "Go module major updates - requires careful review", + "matchDatasources": ["go"], + "matchUpdateTypes": ["major"], + "labels": ["golang", "major-update", "needs-review"], + "automerge": false + } + ], + "golang": { + "enabled": true + }, + "postUpgradeTasks": { + "commands": [ + "go mod tidy", + "go build ./...", + "go test ./..." + ], + "fileFilters": ["go.mod", "go.sum"], + "executionMode": "branch" + }, + "commitMessageAction": "Update", + "commitMessageTopic": "{{depName}}", + "commitMessageExtra": "to {{newVersion}}", + "branchPrefix": "renovate/" +} diff --git a/go.mod b/go.mod index b93093762..60229ccbf 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,10 @@ go 1.24.5 require ( cloud.google.com/go/auth v0.17.0 - github.com/aws/aws-sdk-go-v2 v1.39.3 + github.com/aws/aws-sdk-go-v2 v1.39.5 github.com/aws/smithy-go v1.23.1 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 - github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97 - github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 + github.com/charmbracelet/x/exp/slice v0.0.0-20251103210727-681bf553bc2e github.com/charmbracelet/x/json v0.2.0 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/google/uuid v1.6.0 @@ -17,7 +16,8 @@ require ( github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v4 v4.0.0-rc.2 golang.org/x/oauth2 v0.32.0 - gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117 + google.golang.org/genai v0.0.0-20250923194548-a075d35ad44a + gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250926081054-3f654157e4a1 ) require ( diff --git a/go.sum b/go.sum index 2109b3939..88447a4c4 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/aws/aws-sdk-go-v2 v1.39.3 h1:h7xSsanJ4EQJXG5iuW4UqgP7qBopLpj84mpkNx3wPjM= -github.com/aws/aws-sdk-go-v2 v1.39.3/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= +github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w= +github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= @@ -44,10 +44,8 @@ github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 h1:rwLdEpG9wE6kL69KkEKDiWprO8pQOZHZXeod6+9K+mw= github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4= -github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97 h1:HK7B5Q+0FidxjQD5CovniMw7axkUeMHwgVkxkbmiW/s= -github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97/go.mod h1:ZagL2esO4qxlOJBj0d4PVvLM82akQFtne8s3ivxBnTQ= -github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms= +github.com/charmbracelet/x/exp/slice v0.0.0-20251103210727-681bf553bc2e h1:Sat/5RjbXyJdUiog4+swX3c9TFXogTphzNvhpMqmeTM= +github.com/charmbracelet/x/exp/slice v0.0.0-20251103210727-681bf553bc2e/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ= github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -139,6 +137,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/genai v0.0.0-20250923194548-a075d35ad44a h1:nLGFv9SOTI/hzbwB/b7B1G3r++N0m/mYGIMUdZDwEJ8= +google.golang.org/genai v0.0.0-20250923194548-a075d35ad44a/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= @@ -148,7 +148,7 @@ google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117 h1:fbE/sTnBb9UNfE8cJsOzrYYPqVWVHb7jWH4SI1W//cM= -gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117/go.mod h1:YuVT9NPq7t3oT2WpUemB0DbNL7djIjgajZycxoDLnqs= +gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250926081054-3f654157e4a1 h1:7qjRb7osT+70RzxtMFS1YXuYk2c5JIlEDzrRWDu7kRU= +gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250926081054-3f654157e4a1/go.mod h1:YuVT9NPq7t3oT2WpUemB0DbNL7djIjgajZycxoDLnqs= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/providers/google/google.go b/providers/google/google.go index 2cda50752..cc2b35e06 100644 --- a/providers/google/google.go +++ b/providers/google/google.go @@ -13,9 +13,9 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" "cloud.google.com/go/auth" - "github.com/charmbracelet/go-genai" "github.com/charmbracelet/x/exp/slice" "github.com/google/uuid" + "google.golang.org/genai" ) // Name is the name of the Google provider. diff --git a/providers/openai/language_model_hooks.go b/providers/openai/language_model_hooks.go index 686c655d5..8f3144758 100644 --- a/providers/openai/language_model_hooks.go +++ b/providers/openai/language_model_hooks.go @@ -99,6 +99,11 @@ func DefaultPrepareCallFunc(model fantasy.LanguageModel, params *openai.ChatComp } params.Metadata = metadata } + + // Apply extra fields for custom OpenAI-compatible APIs (e.g., Z.AI GLM thinking mode) + if providerOptions.ExtraFields != nil && len(providerOptions.ExtraFields) > 0 { + params.SetExtraFields(providerOptions.ExtraFields) + } if providerOptions.PromptCacheKey != nil { params.PromptCacheKey = param.NewOpt(*providerOptions.PromptCacheKey) } diff --git a/providers/openai/provider_options.go b/providers/openai/provider_options.go index 9217c6627..8ace202d5 100644 --- a/providers/openai/provider_options.go +++ b/providers/openai/provider_options.go @@ -47,6 +47,9 @@ type ProviderOptions struct { SafetyIdentifier *string `json:"safety_identifier"` ServiceTier *string `json:"service_tier"` StructuredOutputs *bool `json:"structured_outputs"` + // ExtraFields allows passing arbitrary additional parameters to OpenAI-compatible APIs + // that require custom fields not part of the standard OpenAI API specification. + ExtraFields map[string]any `json:"extra_fields,omitempty"` } // Options implements the ProviderOptions interface. diff --git a/providers/openaicompat/language_model_hooks.go b/providers/openaicompat/language_model_hooks.go index 5fa068185..7d47b89f0 100644 --- a/providers/openaicompat/language_model_hooks.go +++ b/providers/openaicompat/language_model_hooks.go @@ -41,6 +41,12 @@ func PrepareCallFunc(_ fantasy.LanguageModel, params *openaisdk.ChatCompletionNe if providerOptions.User != nil { params.User = param.NewOpt(*providerOptions.User) } + + // Apply extra fields for custom OpenAI-compatible APIs (e.g., Z.AI GLM thinking mode) + if providerOptions.ExtraFields != nil && len(providerOptions.ExtraFields) > 0 { + params.SetExtraFields(providerOptions.ExtraFields) + } + return nil, nil } diff --git a/providers/openaicompat/provider_options.go b/providers/openaicompat/provider_options.go index 89dfc61b9..4c91bf856 100644 --- a/providers/openaicompat/provider_options.go +++ b/providers/openaicompat/provider_options.go @@ -10,6 +10,9 @@ import ( type ProviderOptions struct { User *string `json:"user"` ReasoningEffort *openai.ReasoningEffort `json:"reasoning_effort"` + // ExtraFields allows passing arbitrary additional parameters to custom OpenAI-compatible APIs + // that require custom fields not part of the standard OpenAI API specification. + ExtraFields map[string]any `json:"extra_fields,omitempty"` } // ReasoningData represents reasoning data for OpenAI-compatible provider.